<?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" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ React Native - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ React Native - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 25 Aug 2026 13:28:15 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/react-native/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How Neural Machine Translation Works: Build Your Own Translation App with React Native and QVAC ]]>
                </title>
                <description>
                    <![CDATA[ For the past 10 years, we've experienced a massive improvement in translation technologies. We went from robotic-like translations to systems that not only understand the meaning of each word in a sen ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-neural-machine-translation-works-build-your-own-translation-app-with-react-native-and-qvac/</link>
                <guid isPermaLink="false">6a5a5daeee4c6fc82387d36e</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Machine Learning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ nlp ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Mobile Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jibril-M🍀 ]]>
                </dc:creator>
                <pubDate>Fri, 17 Jul 2026 16:51:58 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/89b0a610-cd98-4112-95cc-fb01597911dc.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>For the past 10 years, we've experienced a massive improvement in translation technologies. We went from robotic-like translations to systems that not only understand the meaning of each word in a sentence, but also how the word fits into the context of the full sentence.</p>
<p>For instance, current translation systems know how to differentiate the meaning of "bank" in a sentence like:</p>
<blockquote>
<p>"I can't make the bank deposit today," and "We shall meet near the river bank."</p>
</blockquote>
<p>Both sentences have "bank" in them, but with different meanings.</p>
<p>So how did we get here? This huge revolution started back in June of 2017 when a team of 8 Google researchers, notoriously known as the "8 Samurai," released a research paper titled <a href="https://arxiv.org/abs/1706.03762">"Attention Is All You Need"</a>. This date marked a turning point in modern AI systems and architecture.</p>
<p>For context, this framework is the bedrock of current LLMs like ChatGPT and all large language models.</p>
<p><em>The 8 Google researchers who created the Transformer architecture</em></p>
<img src="https://cdn.hashnode.com/uploads/covers/68e4f3e9867c1707d1b057a9/3826d677-eee8-41bf-ae03-9ab6e805e6f6.png" alt="The 8 Google researchers who created the Transformer architecture" style="display:block;margin:0 auto" width="1185" height="1062" loading="lazy">

<p>So, what is NMT, and how were Google engineers able to develop a framework that enables machines to understand the semantic meaning of each word in a sentence?</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-demystifying-nmt-the-brain-behind-the-screen">Demystifying NMT: The Brain Behind the Screen</a></p>
</li>
<li><p><a href="#heading-how-the-transformer-sees-the-world">How the Transformer Sees the World</a></p>
</li>
<li><p><a href="#heading-why-this-matters">Why This Matters</a></p>
</li>
<li><p><a href="#heading-the-democratization-of-ai">The Democratization of AI</a></p>
</li>
<li><p><a href="#heading-what-is-qvac">What is QVAC?</a></p>
</li>
<li><p><a href="#heading-the-architecture-supported-by-qvac">The Architecture Supported by QVAC</a></p>
</li>
<li><p><a href="#heading-the-inference-pipeline">The Inference Pipeline</a></p>
</li>
<li><p><a href="#heading-setting-up-the-project">Setting Up the Project</a></p>
</li>
<li><p><a href="#heading-complete-implementation">Complete Implementation</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-resources-and-further-reading">Resources and Further Reading</a></p>
</li>
</ul>
<h2 id="heading-demystifying-nmt-the-brain-behind-the-screen">Demystifying NMT: The Brain Behind the Screen</h2>
<p>To understand this breakthrough, we first have to pull back the curtain on what <strong>NMT</strong> (Neural Machine Translation) actually means.</p>
<p>For decades, computer translation was "rule-based." The computer was essentially given a massive bilingual dictionary and a set of grammar rules. It would translate a sentence word-by-word, swap a few positions around, and hope for the best.</p>
<p>This is why early translations felt so incredibly stiff and robotic: the computer was trying to solve language like a math problem.</p>
<p>NMT changed the game by introducing <strong>Neural Networks</strong>, computer systems inspired by the human brain. Instead of memorizing strict rules, an NMT system learns by looking at millions of existing human translations. It looks at how humans translate phrases, captures patterns, and learns how words actually interact in the real world.</p>
<p>But even early NMT systems had a massive flaw: they read sentences sequentially, from left to right. If a sentence was too long, the system would "forget" how it started by the time it reached the end.</p>
<p>This is where the Google researchers made their historic leap.</p>
<h2 id="heading-how-the-transformer-sees-the-world">How the Transformer Sees the World</h2>
<p>The "Attention Is All You Need" paper solved the memory problem by introducing a brand-new architecture called the <strong>Transformer</strong>. Instead of reading a sentence word-by-word, the Transformer reads the entire sentence all at once.</p>
<p>To do this, it splits the job into two main parts: the Encoder and the Decoder.</p>
<h3 id="heading-the-encoder-the-reader">The Encoder (The Reader)</h3>
<p>Think of the Encoder as a highly analytical reader. When you feed a sentence into the system, the Encoder’s job is to read it and build a "mental map" of what the sentence actually means.</p>
<p>It does this using a mechanism called <strong>Self-Attention</strong>. You can think of Self-Attention as a series of spotlights. When the computer looks at a specific word, it shines spotlights on all the other words in the sentence to see how they relate.</p>
<p>Going back to our earlier example:</p>
<blockquote>
<p>"We shall meet near the river bank."</p>
</blockquote>
<p>When the Encoder processes the word <strong>"bank,"</strong> its Self-Attention spotlight instantly flags the word <strong>"river."</strong> Because those two words are highly connected on the AI's mental map, the system immediately knows we're talking about land next to water, not a financial institution. It locks in this "semantic meaning" before moving to the next step.</p>
<h3 id="heading-the-decoder-the-writer">The Decoder (The Writer)</h3>
<p>Once the Encoder has mapped out the true meaning of the sentence, it hands this blueprint over to the <strong>Decoder</strong>.</p>
<p>The Decoder is the writer. Its only job is to translate that blueprint into the target language. But it doesn't just output a pre-written template. It builds the new sentence word-by-word, constantly looking back at the Encoder's blueprint (using a trick called <strong>Cross-Attention</strong>) to make sure it maintains the correct context, tone, and grammar.</p>
<p>If it's translating our river bank sentence into French, it knows to write <em>"la rive"</em> (the bank of the river) instead of <em>"la banque"</em> (the financial bank), because the Encoder's blueprint warned it ahead of time.</p>
<h2 id="heading-why-this-matters">Why This Matters</h2>
<p>By teaching machines to look at the whole picture rather than individual words, Google’s engineers didn't just build a better translator. They built a system that finally understands the nuances, idioms, and context of human language.</p>
<p>And as it turns out, if an AI can understand the context of a sentence well enough to translate it, it can also use that same context to write essays, answer complex questions, and code. The 2017 translation engine accidentally became the foundation of the entire AI era.</p>
<h2 id="heading-the-democratization-of-ai">The Democratization of AI</h2>
<p>A few years after the Transformer's invention, building with it was strictly a toy for the rich. If you wanted to implement even a simple translation feature, you had to pay Big Tech giants like Google a fortune once you went beyond their tiny free tier.</p>
<p>Trying to bypass their dominance was almost impossible because there were practically no resources for independent developers. Back then, just understanding the basic math of a Transformer required an academic PhD. Without a massive research department at your back, trying to build your own solution from scratch was an incredibly expensive nightmare.</p>
<p>Thankfully, the open-source developer community has worked tirelessly to democratize access to AI. Today, we have incredibly powerful models that anyone can download and use freely.</p>
<p>On top of that, the processors in our personal devices have become exceptionally capable. This hardware evolution means that sophisticated AI models can now run locally directly on your smartphone, ensuring maximum data privacy and removing the dependency on external servers.</p>
<p>As the saying goes, <em>"Today it needs a full building to function, tomorrow it will fit in your pocket."</em> Of course, I totally made that quote up 😅, but you get my point!</p>
<p>To put this in action, we'll build a mobile application with Expo and QVAC that translates English to French.</p>
<h2 id="heading-what-is-qvac">What is QVAC?</h2>
<p>QVAC (QuantumVerse Automatic Computer) is a decentralized, local-first AI development platform and SDK created by Tether.</p>
<p>Unlike traditional AI tools that require cloud connectivity, QVAC allows users to run AI models entirely on their own devices. By keeping the computation local and offline, it ensures your data remains private, secure, and entirely under your control.</p>
<h3 id="heading-key-concepts-for-on-device-translation">Key Concepts for On-Device Translation</h3>
<p>To understand how QVAC runs on a mobile device, we must keep a few key concepts in mind:</p>
<h4 id="heading-1-on-device-inference">1. On-Device Inference:</h4>
<p>Running model calculations locally. Rather than relying on a single engine or cloud API, QVAC supports specialized local inference backends depending on the task.</p>
<p>For translation, it uses the Bergamot engine under the hood. These engines memory-map quantized model weights directly into the device's RAM and run calculations using native hardware acceleration.</p>
<h4 id="heading-2-quantization">2. Quantization</h4>
<p>A mathematical optimization technique that compresses the model's weights. This makes it possible for models to fit into the memory constraints of consumer mobile hardware while keeping output quality high.</p>
<h2 id="heading-the-architecture-supported-by-qvac">The Architecture Supported by QVAC</h2>
<p>Before writing code, it's crucial to understand what's actually happening under the hood. To handle local execution without melting your device, the QVAC SDK manages the hardware binding and model lifecycle while hooking into optimized inference backends.</p>
<p>For translation, QVAC utilizes the Bergamot engine. Originally developed as part of the Bergamot project (which powers Firefox's offline translation), this engine is highly optimized for fast, accurate Neural Machine Translation (NMT) on consumer hardware.</p>
<p>At its core, the Bergamot engine takes a source sentence, processes it through its Encoder-Decoder transformer architecture, and predicts the target language tokens in a highly efficient manner.</p>
<h3 id="heading-understanding-language-pairs">Understanding Language Pairs</h3>
<p>It's important to understand the mechanics of how these models are trained. Translation models like the ones used by Bergamot are strictly unidirectional language pairs. This means the <code>BERGAMOT_EN_FR</code> model is designed exclusively to translate from English to French. It can't reverse the process.</p>
<p>If you want to translate French back to English, you would need to download and load a completely separate model trained specifically for that direction.</p>
<p>If a model is trained to be bidirectional (English ↔ French) or multilingual (translating dozens of languages like large language models do), it has to store mathematical representations, vocabulary, and grammar rules for multiple linguistic directions inside a single neural network. This balloons the parameter count, making the file size massive and requiring heavy RAM and compute power to process.</p>
<p>By isolating the task to a single direction (for example <code>BERGAMOT_EN_FR</code>), the model only needs the neural network to "understand" English inputs and "generate" French outputs. It doesn't need the capacity to generate English text.</p>
<p>This extreme specialization is exactly how Bergamot shrinks the model weights down to those incredibly tiny 15–35MB files that can run instantly on a local CPU without freezing your browser.</p>
<h2 id="heading-the-inference-pipeline">The Inference Pipeline</h2>
<p>To visualize how we interact with the translation engine in our codebase, think of local translation as running a dedicated interpreter right in your phone's memory:</p>
<ol>
<li><p><strong>Hiring the interpreter (loading the model):</strong> We map the compressed model file (in this case, the <code>BERGAMOT_EN_FR</code> English-to-French model) directly into the device's RAM.</p>
</li>
<li><p><strong>Handing over the script (text input):</strong> We pass the source text to the loaded engine.</p>
</li>
<li><p><strong>The performance (inference):</strong> The engine reads the text and mathematically predicts the translated tokens, providing the translated result once the process is complete.</p>
</li>
<li><p><strong>Closing the show (unloading):</strong> Because neural network models are memory-intensive, the model can be cleared from RAM to free up resources once the translation is complete or when the user leaves the screen.</p>
</li>
</ol>
<h2 id="heading-setting-up-the-project">Setting Up the Project</h2>
<p>To ensure this guide is completely self-contained, let's start by quickly generating our new Expo application and installing the QVAC SDK. Open your terminal and run the following commands:</p>
<pre><code class="language-bash">npx create-expo-app translator-app --template blank-typescript
cd translator-app
npm install @qvac/sdk jiti
</code></pre>
<p>Next, you need to add the following peer dependencies to your <code>package.json</code> for QVAC to work correctly. Add these lines to their respective sections:</p>
<pre><code class="language-json">  "dependencies": {
    "bare-rpc": "^1.0.0",
    "react-native-bare-kit": "^0.11.5"
  },
  "devDependencies": {
    "bare-pack": "^1.5.1"
  }
</code></pre>
<p>Once added, install the dependencies by running:</p>
<pre><code class="language-bash">npm install
npx expo install expo-file-system expo-build-properties expo-device
</code></pre>
<h3 id="heading-configuring-the-expo-plugin-with-jiti">Configuring the Expo Plugin with JITI</h3>
<p>Next, we need to add the QVAC SDK plugin to our Expo project. Because the QVAC SDK's Expo plugin is distributed as a modern ECMAScript Module (ESM), but Expo's configuration file (<code>app.config.js</code>) runs in a standard Node.js CommonJS environment, we can't use a standard <code>require()</code>.</p>
<p>This is why we installed <code>jiti</code>. It acts as a bridge, allowing us to synchronously load ESM modules inside CommonJS files without breaking the build process.</p>
<p>Create or update your <code>app.config.js</code> file at the root of your project and configure it like this:</p>
<pre><code class="language-javascript">const createJiti = require("jiti");
const jiti = createJiti(__filename);

// Synchronously require the ESM module using jiti
const qvacModule = jiti("@qvac/sdk/expo-plugin");
const withQvacSDK = qvacModule.withQvacSDK || qvacModule.default;

// (Include your withEscapeBundleShellScript helper if needed)

module.exports = ({ config }) =&gt; {
  config.plugins = [
    [
      "expo-build-properties",
      {
        android: { minSdkVersion: 29 },
      },
    ],
    withQvacSDK,
    "expo-router",
    [
      "expo-splash-screen",
      {
        backgroundColor: "#208AEF",
      },
    ],
    withEscapeBundleShellScript, // Custom helper if applicable
  ];

  return config;
};
</code></pre>
<p>This configuration applies the QVAC native setup scripts and ensures Android requires at least SDK version 29 (which is necessary for the native libraries).</p>
<p>With our base configuration ready to go, let's jump straight into the translation code.</p>
<h2 id="heading-complete-implementation">Complete Implementation</h2>
<p>Let's bring it all together. We'll implement an interface that takes English text, manages the downloading and loading states for the Bergamot engine, translates the text to French, and renders the output to the screen.</p>
<p>Replace your entry app file <code>src/app/index.tsx</code> with the following implementation:</p>
<pre><code class="language-tsx">import { View, ScrollView, TextInput, Text, TouchableOpacity, StyleSheet } from "react-native";
import { useState, useEffect } from "react";
import {
  loadModel,
  translate,
  unloadModel,
  BERGAMOT_EN_FR,
  getModelInfo,
} from "@qvac/sdk";
import { Stack } from "expo-router";

type TranslationStatus =
  | "Idle"
  | "Checking model..."
  | "Downloading model..."
  | "Model downloaded successfully."
  | "Loading model..."
  | "Translating..."
  | "Streaming translation..."
  | "Translation finished."
  | `Error: ${string}`;

export default function HomeScreen() {
  const [status, setStatus] = useState&lt;TranslationStatus&gt;("Checking model...");
  const [translatedText, setTranslatedText] = useState&lt;string&gt;("");
  const [inputText, setInputText] = useState&lt;string&gt;("");
  const [isTranslating, setIsTranslating] = useState&lt;boolean&gt;(false);
  const [isDownloaded, setIsDownloaded] = useState&lt;boolean | null&gt;(null);
  const [downloadProgressStr, setDownloadProgressStr] = useState&lt;string&gt;("");

  useEffect(() =&gt; {
    const checkModelStatus = async () =&gt; {
      try {
        const model = await getModelInfo({ name: BERGAMOT_EN_FR.name });
        setIsDownloaded(model.isCached);
        console.log("Model", model);
        setStatus("Idle");
      } catch (error) {
        console.error("Error checking model:", error);
        setStatus("Error: Failed to check model status");
      }
    };
    checkModelStatus();
  }, []);

  const handleDownload = async () =&gt; {
    try {
      setIsTranslating(true);
      setStatus("Downloading model...");
      setDownloadProgressStr("");

      const modelId = await loadModel({
        modelSrc: BERGAMOT_EN_FR,
        modelType: "nmt",
        onProgress: (progress: any) =&gt; {
          let pct = progress.percentage;
          let dl = progress.downloaded;
          let tot = progress.total;
          if (progress.shardInfo) {
            pct = progress.shardInfo.overallPercentage;
            dl = progress.shardInfo.overallDownloaded;
            tot = progress.shardInfo.overallTotal;
          }
          const formatBytes = (bytes: number) =&gt; {
            if (bytes === 0) return "0 B";
            const k = 1024;
            const sizes = ["B", "KB", "MB", "GB"];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return (
              parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
            );
          };
          setDownloadProgressStr(
            `${pct.toFixed(2)}% (${formatBytes(dl)} / ${formatBytes(tot)})`,
          );
        },
        modelConfig: {
          engine: "Bergamot",
          from: "en",
          to: "fr",
          beamsize: 1,
          normalize: 1,
          temperature: 0.2,
          norepeatngramsize: 3,
          lengthpenalty: 1.2,
        },
      });

      await unloadModel({ modelId, clearStorage: false });

      setIsDownloaded(true);
      setStatus("Model downloaded successfully.");
    } catch (error: any) {
      console.error(error);
      setStatus(`Error: ${error.message}`);
    } finally {
      setIsTranslating(false);
      setDownloadProgressStr("");
    }
  };

  const handleTranslate = async () =&gt; {
    if (!inputText.trim()) {
      setStatus("Error: Please enter text to translate");
      return;
    }

    try {
      setIsTranslating(true);
      setTranslatedText("");
      setStatus("Loading model...");

      const modelId = await loadModel({
        modelSrc: BERGAMOT_EN_FR,
        modelType: "nmt",

        modelConfig: {
          engine: "Bergamot",
          from: "en",
          to: "fr",
          beamsize: 1,
          normalize: 1,
          temperature: 0.2,
          norepeatngramsize: 3,
          lengthpenalty: 1.2,
        },
      });

      setStatus(`Translating...`);

      const result = translate({
        modelId,
        text: inputText,
        modelType: "nmt",
        stream: false,
      });

      const text = await result.text;
      setTranslatedText(text);

      const stats = await result.stats;
      if (stats) {
        console.log(`▸ Processing stats:`, stats);
      }

      setStatus("Translation finished.");

      await unloadModel({ modelId, clearStorage: false });
    } catch (error: any) {
      console.error(error);
      setStatus(`Error: ${error.message}`);
    } finally {
      setIsTranslating(false);
    }
  };

  return (
    &lt;&gt;
      &lt;Stack.Screen
        options={{
          headerTitle: "Translator",
          headerStyle: { backgroundColor: "#000" },
          headerTintColor: "#fff",
        }}
      /&gt;
      &lt;ScrollView contentContainerStyle={styles.scrollContainer}&gt;
        &lt;View style={styles.card}&gt;
          &lt;View style={styles.header}&gt;
            &lt;Text style={styles.title}&gt;
              English to French Translator
            &lt;/Text&gt;
            &lt;Text style={styles.subtitle}&gt;
              Enter text to translate:
            &lt;/Text&gt;
          &lt;/View&gt;

          &lt;View style={styles.content}&gt;
            &lt;TextInput
              style={[styles.input, isTranslating &amp;&amp; styles.disabledText]}
              multiline
              placeholder="Type English text here..."
              placeholderTextColor="#888"
              value={inputText}
              onChangeText={setInputText}
              editable={!isTranslating}
            /&gt;

            &lt;Text style={styles.statusText}&gt;
              Status: {status}
              {downloadProgressStr ? `\n${downloadProgressStr}` : ""}
            &lt;/Text&gt;

            {isDownloaded === null ? (
              &lt;TouchableOpacity disabled style={[styles.button, styles.buttonDisabled]}&gt;
                &lt;Text style={styles.buttonText}&gt;
                  Loading...
                &lt;/Text&gt;
              &lt;/TouchableOpacity&gt;
            ) : isDownloaded ? (
              &lt;TouchableOpacity
                onPress={handleTranslate}
                style={[
                  styles.button,
                  (isTranslating || !inputText.trim()) &amp;&amp; styles.buttonDisabled,
                ]}
                disabled={isTranslating || !inputText.trim()}
              &gt;
                &lt;Text style={styles.buttonText}&gt;
                  {isTranslating ? "Translating..." : "Translate"}
                &lt;/Text&gt;
              &lt;/TouchableOpacity&gt;
            ) : (
              &lt;TouchableOpacity
                onPress={handleDownload}
                style={[styles.button, isTranslating &amp;&amp; styles.buttonDisabled]}
                disabled={isTranslating}
              &gt;
                &lt;Text style={styles.buttonText}&gt;
                  {isTranslating ? "Downloading..." : "Download Model"}
                &lt;/Text&gt;
              &lt;/TouchableOpacity&gt;
            )}

            &lt;View style={styles.outputContainer}&gt;
              &lt;Text style={styles.outputText}&gt;
                {translatedText || "Translation will appear here..."}
              &lt;/Text&gt;
            &lt;/View&gt;
          &lt;/View&gt;
        &lt;/View&gt;
      &lt;/ScrollView&gt;
    &lt;/&gt;
  );
}

const styles = StyleSheet.create({
  scrollContainer: {
    flexGrow: 1,
    paddingHorizontal: 16,
    paddingTop: 16,
    paddingBottom: 24,
    backgroundColor: "#f9fafb",
  },
  card: {
    backgroundColor: "#ffffff",
    maxWidth: 450,
    width: "100%",
    alignSelf: "center",
    borderRadius: 12,
    padding: 16,
  },
  header: {
    marginBottom: 16,
  },
  title: {
    textAlign: "center",
    fontSize: 24,
    fontWeight: "bold",
    color: "#111827",
  },
  subtitle: {
    textAlign: "center",
    marginTop: 4,
    fontSize: 16,
    color: "#6b7280",
  },
  content: {
    gap: 24,
  },
  input: {
    borderWidth: 1,
    borderColor: "#e5e7eb",
    backgroundColor: "#ffffff",
    color: "#111827",
    padding: 12,
    borderRadius: 8,
    minHeight: 100,
    textAlignVertical: "top",
  },
  disabledText: {
    opacity: 0.5,
  },
  statusText: {
    fontSize: 14,
    color: "#3b82f6",
    fontWeight: "bold",
    textAlign: "center",
    marginTop: 12,
    marginBottom: 12,
  },
  button: {
    width: "100%",
    height: 48,
    borderRadius: 12,
    backgroundColor: "#3b82f6",
    alignItems: "center",
    justifyContent: "center",
  },
  buttonDisabled: {
    opacity: 0.5,
  },
  buttonText: {
    fontWeight: "600",
    fontSize: 18,
    color: "#ffffff",
  },
  outputContainer: {
    marginTop: 16,
    padding: 16,
    backgroundColor: "#f3f4f6",
    borderRadius: 8,
    minHeight: 100,
  },
  outputText: {
    fontSize: 16,
    color: "#111827",
  },
});
</code></pre>
<p>Here is a translation example from the application.</p>
<p><em>Input</em> (English)</p>
<blockquote>
<p>The location I told you was near the river bank</p>
</blockquote>
<p><em>Output</em> (French)</p>
<blockquote>
<p>L'endroit où je vous ai dit était près de la rive de la rivière</p>
</blockquote>
<h3 id="heading-codebase-breakdown">Codebase Breakdown</h3>
<p>Let’s lift the hood on how this local translation implementation manages native model lifecycles and processes the streamed tokens.</p>
<h4 id="heading-1-managing-the-native-lifecycle">1. Managing the Native Lifecycle</h4>
<p>Loading neural network weights for translation is computationally expensive. When the QVAC runtime initializes a model, it must read parameters from the local disk and copy the active weights into device RAM.</p>
<p>To handle this efficiently, we check if the model is cached before attempting to load it. This is used to check if the model is downloaded. That's the meaning of cached: it means the model has been downloaded to the user's disk:</p>
<pre><code class="language-typescript">const model = await getModelInfo({ name: BERGAMOT_EN_FR.name });
setIsDownloaded(model.isCached);
</code></pre>
<p>The <code>loadModel</code> function will automatically handle downloading the model from the Hugging Face hub if it hasn't been cached locally yet. Once the file is available locally, it directly memory-maps the weights.</p>
<h4 id="heading-2-translating-the-text">2. Translating the Text</h4>
<p>Once the model is loaded, we can pass our text to the translation engine:</p>
<pre><code class="language-typescript">const result = translate({
  modelId,
  text: inputText,
  modelType: "nmt",
  stream: false,
});

const text = await result.text;
setTranslatedText(text);
</code></pre>
<p>This waits for the full translation to complete before displaying the final result to the user.</p>
<h4 id="heading-3-unloading-the-model">3. Unloading the Model</h4>
<p>After the translation is complete, we explicitly destroy the model via <code>unloadModel</code>:</p>
<pre><code class="language-typescript">await unloadModel({ modelId, clearStorage: false });
</code></pre>
<p>By unloading the model, we ensure that the device's RAM is freed up for other processes. Because the model is already downloaded and cached on the disk (and we explicitly set <code>clearStorage: false</code>), reloading the model the next time the user wants to translate something will be nearly instantaneous.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Transitioning translation from the cloud to on-device hardware offers a practical approach for mobile application developers. Running model inference locally eliminates reliance on remote internet connectivity, removes recurring API usage costs, and ensures that user text inputs never leave the physical device.</p>
<p>Integrating local translation can be highly beneficial for travel apps, secure communication tools, or educational platforms. As edge processors gain dedicated hardware acceleration cores and open-source models become even more efficient through quantization research, local-first architectures present a compelling alternative for developers prioritizing privacy, offline resilience, and predictable cost structures.</p>
<h2 id="heading-resources-and-further-reading">Resources and Further Reading</h2>
<p>To dive deeper into local Neural Machine Translation, inspect the source code, or explore advanced configurations for your mobile applications, check out the following resources:</p>
<ul>
<li><p><a href="https://docs.qvac.tether.io/ai-capabilities/translation/"><strong>QVAC Translation Docs</strong></a>: Official documentation for integrating local translation capabilities with QVAC.</p>
</li>
<li><p><a href="https://docs.qvac.tether.io/tutorials/expo/"><strong>QVAC Expo Integration Docs</strong></a>: Learn more about configuring custom local models in Expo.</p>
</li>
<li><p><a href="https://browser.mt/"><strong>Bergamot Project</strong></a>: Learn more about the underlying Neural Machine Translation engine.</p>
</li>
<li><p><a href="https://arxiv.org/abs/1706.03762"><strong>Attention Is All You Need</strong></a>: The original 2017 Google research paper that introduced the Transformer architecture.</p>
</li>
<li><p><a href="https://github.com/DjibrilM/en-fr-translator-Article-project-"><strong>Full Code Example</strong></a>: Full code example's repository.</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Master Full-Stack Mobile Development with React Native ]]>
                </title>
                <description>
                    <![CDATA[ Do you want to get into mobile development and build cross-platform applications? We just published a comprehensive new course on the freeCodeCamp.org YouTube channel that will teach you how to build  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/master-full-stack-mobile-development-with-react-native/</link>
                <guid isPermaLink="false">6a58c1695b624eb755f699d2</guid>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Thu, 16 Jul 2026 11:32:57 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5f68e7df6dfc523d0a894e7c/823b7020-e45c-499e-99d2-d7324fbb2533.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Do you want to get into mobile development and build cross-platform applications? We just published a comprehensive new course on the freeCodeCamp.org YouTube channel that will teach you how to build a complete, full-stack React Native application from the ground up.</p>
<p>In this massive tutorial, you will develop a feature-rich grocery list app that runs seamlessly on both iOS and Android.</p>
<p>Throughout the lessons, you will get hands-on experience with some of the most powerful tools in the modern mobile ecosystem. You will use Expo and React Native for the core framework, ensuring your single codebase translates flawlessly across devices. For the backend and data management, the project integrates a Neon Postgres database managed with Drizzle ORM. You will also learn how to implement secure user authentication using Clerk, allowing users to seamlessly log in with Google, Apple, or GitHub.</p>
<p>Styling your mobile application is streamlined in this course, as it walks you through using NativeWind to apply Tailwind CSS classes directly to your React Native components. You will also master global state management with Zustand, making it remarkably simple to handle your application's complex data flow. Towards the end of the build, you will even integrate Sentry to create a native feedback form that captures user bug reports and feature requests in real-time.</p>
<p>You can watch the full course on the <a href="http://freeCodeCamp.org">freeCodeCamp.org</a> YouTube channel (4-hour watch).</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/4GtVeULrNks" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ The React Native Live Activities Handbook: How to Build iOS Live Activities and Android 16 Live Updates ]]>
                </title>
                <description>
                    <![CDATA[ A Live Activity is the card that sits on your lock screen while a delivery rider approaches, updating itself without you opening the app. Apple shipped the API in iOS 16.1. Google shipped its own vers ]]>
                </description>
                <link>https://www.freecodecamp.org/news/react-native-live-activities-handbook/</link>
                <guid isPermaLink="false">6a565e929f58169e255198d4</guid>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Expo ]]>
                    </category>
                
                    <category>
                        <![CDATA[ iOS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Android ]]>
                    </category>
                
                    <category>
                        <![CDATA[ push notifications ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Farouq Seriki ]]>
                </dc:creator>
                <pubDate>Tue, 14 Jul 2026 16:06:42 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/542a6607-0c73-4dbf-822e-d72d2325ef7a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A Live Activity is the card that sits on your lock screen while a delivery rider approaches, updating itself without you opening the app. Apple shipped the API in iOS 16.1. Google shipped its own version, called Live Updates, in Android 16.</p>
<p>The product requirement is identical on both platforms. The contracts underneath are opposites.</p>
<p>On iOS, <a href="https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns">Apple Push Notification service</a> (APNs) updates a system-owned SwiftUI widget for you, and your app code never runs. On Android there's no system-managed remote update at all. A data-only <a href="https://firebase.google.com/docs/cloud-messaging">Firebase Cloud Messaging</a> (FCM) message wakes a background service, and your own code re-posts the notification every single time.</p>
<p>In this handbook you'll build both. You'll write one TypeScript API backed by two native implementations: a Swift one that talks to <a href="https://developer.apple.com/documentation/activitykit">ActivityKit</a> and a Kotlin one that talks to <code>NotificationManager</code>. You'll also write an APNs client and an FCM client from scratch, in about sixty lines each, with no libraries. And you'll learn the silent failure modes that make this API hard, because almost every mistake here produces no error at all.</p>
<p>I built a delivery-tracking demo called DropTrack to work through this. Everything below comes from that build, including a three-device Samsung investigation that ends with a screenshot of a hardcoded allowlist.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-live-activities-and-live-updates-actually-are">What Live Activities and Live Updates Actually Are</a></p>
</li>
<li><p><a href="#heading-where-you-have-already-seen-this-feature">Where You Have Already Seen This Feature</a></p>
</li>
<li><p><a href="#heading-why-you-need-a-custom-native-module">Why You Need a Custom Native Module</a></p>
</li>
<li><p><a href="#heading-how-the-ios-contract-works">How the iOS Contract Works</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-widget-in-swiftui">How to Build the Widget in SwiftUI</a></p>
</li>
<li><p><a href="#heading-how-to-bridge-activitykit-to-javascript">How to Bridge ActivityKit to JavaScript</a></p>
</li>
<li><p><a href="#heading-four-ios-gotchas-that-cost-me-an-evening-each">Four iOS Gotchas That Cost Me an Evening Each</a></p>
</li>
<li><p><a href="#heading-how-to-drive-ios-from-a-server-with-apns">How to Drive iOS From a Server With APNs</a></p>
</li>
<li><p><a href="#heading-how-to-write-an-apns-client-from-scratch">How to Write an APNs Client From Scratch</a></p>
</li>
<li><p><a href="#heading-how-to-test-on-real-hardware">How to Test on Real Hardware</a></p>
</li>
<li><p><a href="#heading-why-android-has-no-activitykit">Why Android Has No ActivityKit</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-kotlin-side">How to Build the Kotlin Side</a></p>
</li>
<li><p><a href="#heading-how-to-drive-android-from-a-server-with-fcm">How to Drive Android From a Server With FCM</a></p>
</li>
<li><p><a href="#heading-three-ux-gaps-the-naive-implementation-leaves">Three UX Gaps the Naive Implementation Leaves</a></p>
</li>
<li><p><a href="#heading-how-to-script-the-simulators-and-devices">How to Script the Simulators and Devices</a></p>
</li>
<li><p><a href="#heading-how-ios-and-android-compare">How iOS and Android Compare</a></p>
</li>
<li><p><a href="#heading-the-samsung-reality-check">The Samsung Reality Check</a></p>
</li>
<li><p><a href="#heading-the-demo-repository">The Demo Repository</a></p>
</li>
<li><p><a href="#heading-what-to-know-before-you-start">What to Know Before You Start</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-sources-and-further-reading">Sources and Further Reading</a></p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along, you'll need:</p>
<ul>
<li><p>Node.js 20 or later.</p>
</li>
<li><p>Xcode 26 or later, plus a <strong>paid Apple Developer account</strong>. APNs requires a real signing key and a device build. The simulator can't receive Live Activity pushes.</p>
</li>
<li><p>Android Studio with an API 36 emulator built from a <code>google_apis</code> system image. Google Play services are required for FCM, and a bare Android Open Source Project image doesn't include them.</p>
</li>
<li><p>A <a href="https://firebase.google.com/docs/cloud-messaging">Firebase</a> project, for the Android half.</p>
</li>
<li><p>Working knowledge of Swift, Kotlin, and TypeScript. This is a native modules article, not a JavaScript-only one.</p>
</li>
</ul>
<p>The versions I used throughout:</p>
<table>
<thead>
<tr>
<th>Package or tool</th>
<th>Version</th>
</tr>
</thead>
<tbody><tr>
<td>Expo SDK</td>
<td>57.0.4</td>
</tr>
<tr>
<td>React Native</td>
<td>0.86.0</td>
</tr>
<tr>
<td>React</td>
<td>19.2.3</td>
</tr>
<tr>
<td>TypeScript</td>
<td>6.0.3</td>
</tr>
<tr>
<td><a href="https://github.com/EvanBacon/expo-apple-targets"><code>@bacons/apple-targets</code></a></td>
<td>4.0.7</td>
</tr>
<tr>
<td><a href="https://developer.android.com/jetpack/androidx/releases/core"><code>androidx.core</code></a></td>
<td>1.17.0</td>
</tr>
<tr>
<td><code>firebase-bom</code></td>
<td>33.7.0</td>
</tr>
<tr>
<td>Xcode</td>
<td>26.3</td>
</tr>
</tbody></table>
<p>The <code>androidx.core</code> version isn't optional. Version 1.17.0 backports the Android 16 promotion APIs so they compile against base SDK 36. I'll explain why in a moment.</p>
<h2 id="heading-what-live-activities-and-live-updates-actually-are">What Live Activities and Live Updates Actually Are</h2>
<p>A Live Activity isn't a push notification. A notification is a fire-and-forget event. A Live Activity is a persistent, glanceable card with its own state that changes in place, a bounded lifetime, and dedicated system surfaces.</p>
<p>Here's where each platform landed:</p>
<ul>
<li><p>iOS 16.1 introduced lock-screen Live Activities. 16.2 added the Dynamic Island. 17.2 added push-to-start. 18 added broadcast channels. See Apple's <a href="https://developer.apple.com/documentation/activitykit">ActivityKit documentation</a> and the <a href="https://developer.apple.com/design/human-interface-guidelines/live-activities">Live Activities Human Interface Guidelines</a>.</p>
</li>
<li><p>Android 16 (API 36) introduced <a href="https://developer.android.com/reference/android/app/Notification.ProgressStyle"><code>Notification.ProgressStyle</code></a>, the segmented progress bar.</p>
</li>
<li><p>Android 16 QPR1 (API 36.1), the first Quarterly Platform Release, introduced the <em>promotion</em> pipeline: the status-bar chip and the elevated lock-screen slot. Google calls the whole feature <a href="https://developer.android.com/about/versions/16/features/progress-centric-notifications">progress-centric notifications</a>.</p>
</li>
</ul>
<p>I built DropTrack to exercise all of it. An order moves through seven steps, from "Order placed" to "Arriving now". A courier can be reassigned mid-run. The card is driven either locally from an in-app console or remotely from a push.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c84fbe8c8c80534346db05/ea8d99d3-162d-4104-8402-c38e4ebc39fb.png" alt="DropTrack expanded Dynamic Island on a real iPhone 14 Pro" style="display:block;margin:0 auto" width="640" height="1387" loading="lazy">

<h2 id="heading-where-you-have-already-seen-this-feature">Where You Have Already Seen This Feature</h2>
<p>Before any code, it helps to know why this API exists, because "a card on the lock screen" undersells it.</p>
<p>Think about the last time you ordered food. You place the order, lock your phone, and then what? Without a Live Activity, the app's only way to reach you is a push notification per state change. Order confirmed. Restaurant is preparing your food. Rider assigned. Rider is two stops away. Rider has arrived.</p>
<p>That's five notifications for one order. Multiply that by every order, and you've trained the user to mute you.</p>
<p>A Live Activity replaces all five with one card that mutates in place. Same information with one surface and no notification fatigue. That's the entire product argument.</p>
<p>Apple's own <a href="https://developer.apple.com/design/human-interface-guidelines/live-activities">Human Interface Guidelines</a> name the recurring use cases: sports scores, workouts, rides, and deliveries. Google's <a href="https://developer.android.com/about/versions/16/features/progress-centric-notifications">progress-centric notifications</a> documentation converges on almost the same list, stating that "key use cases include rideshare, delivery, and navigation." That page even describes the delivery example in terms of progress <em>points</em> for food preparation and delivery milestones, and <em>segments</em> colored by traffic conditions, which is precisely the widget you're about to build.</p>
<h3 id="heading-apps-that-have-shipped-it">Apps That Have Shipped it</h3>
<p>Rather than repeat the roundup posts, I checked each app's own App Store listing, because a company describing its own feature is the strongest evidence available. Every app below states that it uses Live Activities in its own words:</p>
<table>
<thead>
<tr>
<th>App</th>
<th>What its Live Activity shows</th>
<th>First-party source</th>
</tr>
</thead>
<tbody><tr>
<td>Chowdeck</td>
<td>Delivery progress, "right on your Lock Screen and in the Dynamic Island"</td>
<td><a href="https://apps.apple.com/us/app/chowdeck-food-delivery/id1530676376">App Store listing</a></td>
</tr>
<tr>
<td>Flighty</td>
<td>Departure countdown, gate changes, taxi time, arrival progress</td>
<td><a href="https://flighty.com/help/live-activities-widgets">Flighty's own help docs</a></td>
</tr>
<tr>
<td>ESPN</td>
<td>Key plays and game stats for major soccer leagues, the NHL, and the NBA</td>
<td><a href="https://apps.apple.com/us/app/espn-live-sports-scores/id317469184">App Store listing</a></td>
</tr>
<tr>
<td>MLB</td>
<td>Game updates on the lock screen</td>
<td><a href="https://apps.apple.com/us/app/mlb/id493619333">App Store listing</a></td>
</tr>
<tr>
<td>FotMob</td>
<td>Soccer scores on the lock screen</td>
<td><a href="https://apps.apple.com/us/app/fotmob-soccer-live-scores/id488575683">App Store listing</a></td>
</tr>
<tr>
<td>CARROT Weather</td>
<td>Incoming precipitation and storm intensity</td>
<td><a href="https://apps.apple.com/us/app/carrot-weather-alerts-radar/id961390574">App Store listing</a></td>
</tr>
<tr>
<td>Structured</td>
<td>Pomodoro focus timers</td>
<td><a href="https://apps.apple.com/us/app/structured-daily-planner-todo/id1499198946">App Store listing</a></td>
</tr>
</tbody></table>
<p>Apple ships them in its own software, too. The <a href="https://support.apple.com/guide/apple-sports-app/follow-games-in-real-time-apdc0cb7ad64/web">Apple Sports support page</a> says that with Live Activities turned on "you can get real-time information on your iPhone Lock Screen or your Apple Watch so that you can follow every moment of the game."</p>
<p>Two more are well documented by the technology press rather than by the companies themselves. Uber Eats <a href="https://www.macrumors.com/2023/05/02/uber-eats-live-activities/">rolled out Live Activities in May 2023</a>, showing order status, estimated time of arrival, and the driver's name and photo. DoorDash <a href="https://www.macrumors.com/2023/12/04/doordash-rolling-out-live-activities/">followed in December 2023</a> with a real-time estimated time of arrival in the Dynamic Island.</p>
<p>Two honest caveats about that table. First, an app not mentioning Live Activities in its listing doesn't mean it lacks the feature, only that I couldn't confirm it first-hand. Uber Eats is exactly that case. Second, this reflects the listings as I read them, and any app can add or drop the feature in a release.</p>
<p>Food delivery is the canonical case, and it's what DropTrack models. Ride-hailing is the highest-stakes version: "driver arriving in 3 minutes", the plate number, the trip in progress, glanced at while you stand on a curb with a locked phone. The Dynamic Island's compact presentation exists almost for this: a glyph and a percentage read in half a second.</p>
<p>Finance is the case people forget, and it's where a segmented bar would earn its keep. A crypto deposit isn't "pending, then done". It's "3 of 12 network confirmations", which is a segmented progress bar with discrete steps, mapping cleanly onto <a href="https://developer.android.com/reference/android/app/Notification.ProgressStyle"><code>ProgressStyle</code>'s</a> segments and points and onto the SwiftUI capsules you will write below. The same shape fits a bank transfer clearing or a card payment settling.</p>
<p>I want to be careful here, though: I checked the App Store listings for the major exchanges and neobanks and found none that documents a Live Activity. Treat this one as an obvious fit that the category hasn't yet taken up, not as prior art.</p>
<p>Here's the table I wish someone had shown me before I started:</p>
<table>
<thead>
<tr>
<th>Category</th>
<th>What updates</th>
<th>Who drives the update</th>
<th>Why a card beats notifications</th>
</tr>
</thead>
<tbody><tr>
<td>Food delivery</td>
<td>Courier position, ETA, rider swap</td>
<td>Backend</td>
<td>Five pushes collapse into one mutating card</td>
</tr>
<tr>
<td>Ride-hailing</td>
<td>Driver arriving, trip state, fare</td>
<td>Backend</td>
<td>Glanceable while the phone is locked</td>
</tr>
<tr>
<td>Crypto deposit</td>
<td>Confirmations, 3 of 12</td>
<td>Backend</td>
<td>A progress bar, not a binary "done" ping</td>
</tr>
<tr>
<td>Bank transfer</td>
<td>Settlement stage</td>
<td>Backend</td>
<td>Fewer "has it landed?" support tickets</td>
</tr>
<tr>
<td>Parcel</td>
<td>Out for delivery, then delivered</td>
<td>Backend</td>
<td>Persistent, not buried in the tray</td>
</tr>
<tr>
<td>Timer or workout</td>
<td>Elapsed time</td>
<td>The device</td>
<td>No server needed. This is the easy case</td>
</tr>
</tbody></table>
<p>Look at the third column. In every commercially interesting case the update is driven by your server, not by the device. That means the entire value of the feature lives in the push path. And the push path is exactly where the two platforms diverge, where the failures are silent, and where most of this handbook's pain is concentrated.</p>
<p>The pretty SwiftUI card is the easy half. Getting a backend to reliably mutate that card while the app is dead is the hard half, and it's different on each platform.</p>
<h2 id="heading-why-you-need-a-custom-native-module">Why You Need a Custom Native Module</h2>
<p>Before writing a line of Swift, I evaluated the shortcuts. Both fail, for instructive reasons.</p>
<p><a href="https://github.com/software-mansion-labs/expo-live-activity"><code>expo-live-activity</code></a> ships a predefined widget layout and a fixed content-state shape. DropTrack needs a custom segmented progress bar, a rider-reassignment treatment, and its own Dynamic Island layouts. All of those require owning <code>ActivityAttributes</code> and the SwiftUI that renders it. You reach first render fastest, and then you eject.</p>
<p><a href="https://github.com/invertase/notifee">Notifee</a>, the notification library most React Native developers reach for, is archived. As of this writing its GitHub repository is marked archived, its last published release was <a href="https://www.npmjs.com/package/@notifee/react-native"><code>@notifee/react-native@9.1.8</code></a> in December 2024, and its Android module still targets <code>compileSdk 34</code>. It has no Live Updates support. A custom module is currently the only React Native route to Android 16 Live Updates.</p>
<p>So: one local <a href="https://docs.expo.dev/modules/overview/">Expo module</a>, one TypeScript API, two native backends.</p>
<pre><code class="language-text">modules/droptrack-live/
├── expo-module.config.json   ← autolinking (apple + android)
├── index.ts                  ← the public JS API
├── src/                      ← TS types, native binding, web no-op
├── ios/
│   ├── DeliveryAttributes.swift    ← the ActivityKit contract
│   └── DroptrackLiveModule.swift   ← JS to ActivityKit
└── android/
    ├── DroptrackLiveModule.kt      ← JS to NotificationManager
    ├── DeliveryNotifier.kt         ← the shared notification builder
    └── DroptrackFcmService.kt      ← push to NotificationManager
</code></pre>
<p>The JavaScript surface is deliberately small, and identical across platforms:</p>
<pre><code class="language-typescript">// The dynamic half of the card: every update replaces this object wholesale.
// Keep it small. iOS rejects an ActivityKit push payload over 4 KB.
export type DeliveryState = {
  status: string;           // free text shown as the headline: "Picked up"
  progress: number;         // 0.0 to 1.0, drives the bar on both platforms
  etaEpochMillis: number;   // Unix ms, the shape JS speaks. Swift converts it.
  stopsRemaining: number;   // renders as "2 stops away" / "you're next"
  courierName: string;      // dynamic, because riders get reassigned mid-run
  riderReassigned: boolean; // flips the courier row to the "new rider" style
};

// Three verbs, mirroring the ActivityKit lifecycle. Android fakes the same
// shape with notify / notify / cancel, so callers never branch on platform.

// Starts the activity and returns the system-assigned id. Hold on to it:
// every later call needs it, and it does NOT survive an app restart.
startDelivery(info: DeliveryInfo, state: DeliveryState): Promise&lt;string&gt;;

// Replaces the ContentState in place. The card mutates, it does not re-appear.
updateDelivery(id: string, state: DeliveryState): Promise&lt;void&gt;;

// Final state, then dismissal. Omit dismissAfterSeconds to let the card linger.
endDelivery(id: string, state: DeliveryState, dismissAfterSeconds?: number): Promise&lt;void&gt;;

isSupported(): boolean;          // false on iOS &lt; 16.2, Android &lt; 16, and web
areActivitiesEnabled(): boolean; // false if the USER switched them off in Settings
</code></pre>
<p>Those last two are separate on purpose. A device can support Live Activities while the user has switched them off in Settings, and only <code>areActivitiesEnabled()</code> catches that. <code>Activity.request</code> throws in that case, so <code>startDelivery</code> throws too.</p>
<p>The <code>dismissAfterSeconds</code> argument maps onto ActivityKit's <a href="https://developer.apple.com/documentation/activitykit/activityuidismissalpolicy">dismissal policy</a>. Under the default policy, Apple's documentation says "the system keeps a Live Activity that ended on the Lock Screen for up to four hours after it ends." Passing a value shortens that window.</p>
<p>This project uses Expo's <a href="https://docs.expo.dev/workflow/continuous-native-generation/">Continuous Native Generation</a> (CNG), which means the <code>ios</code> and <code>android</code> folders are generated by <code>prebuild</code> and aren't committed. Every line of native code therefore lives in the module or in a <a href="https://docs.expo.dev/config-plugins/introduction/">config plugin</a>, so it survives <code>prebuild --clean</code>. The widget extension itself is generated by <a href="https://github.com/EvanBacon/expo-apple-targets"><code>@bacons/apple-targets</code></a> for the same reason.</p>
<p>One structural detail matters later: the module compiles as its own CocoaPod, not into the app target.</p>
<h2 id="heading-how-the-ios-contract-works">How the iOS Contract Works</h2>
<p>An ActivityKit activity is split in two. <a href="https://developer.apple.com/documentation/activitykit/activityattributes"><code>ActivityAttributes</code></a> holds static data, set once and never changed. Its nested <code>ContentState</code> holds the dynamic half, which every update replaces wholesale.</p>
<p>Most tutorials state that rule and move on. It's worth understanding <em>why</em> the platform forces it, because the reason tells you exactly which half any given field belongs in.</p>
<h3 id="heading-why-the-split-exists">Why the Split Exists</h3>
<p>The split isn't a style convention. It's about what can physically cross the wire.</p>
<p>When you call <code>Activity.request(...)</code>, you hand ActivityKit two things at once: the attributes and the first content state. The system files the attributes away and never accepts a new copy of them. From that moment on, the only thing you can ever send is a new <code>ContentState</code>.</p>
<p>You can see this in the shape of an <a href="https://developer.apple.com/documentation/activitykit/starting-and-updating-live-activities-with-activitykit-push-notifications">ActivityKit push payload</a>. Here's what my APNs client actually transmits:</p>
<pre><code class="language-javascript">aps: {
  // Older-than-last timestamps are discarded by APNs (ordering guard).
  timestamp: Math.floor(Date.now() / 1000),
  event,                        // "update" or "end"
  'content-state': contentState, // the only field that carries data
}
</code></pre>
<p>There is no <code>attributes</code> key. There can't be one. Nothing in the payload can reach the static half.</p>
<p>So "should this field be static or dynamic?" is really the question <strong>"will my server ever need to change this value?"</strong> If the answer is yes, the field must live in <code>ContentState</code>, because that's the only thing a push can move.</p>
<p>That's the whole design. <code>ActivityAttributes</code> is the activity's identity, fixed at birth. <code>ContentState</code> is its current reading, replaced wholesale on every update rather than patched. Send a content state that omits a field and you haven't left that field alone, you've failed to decode. The <code>Codable</code> conformance is what lets the system serialize the state across a process boundary, and the <code>Hashable</code> conformance lets it tell whether an incoming state actually differs from the one already on screen.</p>
<h3 id="heading-the-bet-i-lost">The Bet I Lost</h3>
<p>Here's what I wrote first. It looks reasonable:</p>
<pre><code class="language-swift">// BEFORE: courierName is static. This compiles, ships, and works fine,
// right up until a dispatcher reassigns the rider mid-delivery.
struct DeliveryAttributes: ActivityAttributes {
  public struct ContentState: Codable, Hashable {
    var status: String
    var progress: Double
    var eta: Date
    var stopsRemaining: Int
  }
  var orderId: String
  var courierName: String   // the bet: a delivery has one courier
}
</code></pre>
<p>Putting a field in the static half is a bet that it can never change for the activity's lifetime. I bet that a delivery has one courier. Then I built rider reassignment, where a dispatcher swaps the courier mid-run, and the bet came due.</p>
<p>Because <code>courierName</code> was static, there was no way to express the change. Not through <code>activity.update()</code>, not through a push, not at all. The only escape would have been to end the activity and start a new one, which means the card disappears from the lock screen and returns as a different card, mid-delivery. That's not a fix, it's a regression.</p>
<p>So the field moved:</p>
<pre><code class="language-swift">// AFTER: only orderId is genuinely immutable. Everything a dispatcher
// can change lives in ContentState, because ContentState is the only
// thing an APNs push can carry.
struct DeliveryAttributes: ActivityAttributes {
  public struct ContentState: Codable, Hashable {
    /// Human-readable status, e.g. "Picked up", "2 stops away"
    var status: String
    /// Overall delivery progress, 0.0 ... 1.0
    var progress: Double
    /// Estimated arrival time
    var eta: Date
    /// Stops before the courier reaches the user
    var stopsRemaining: Int
    /// Courier display name, dynamic since riders can be reassigned mid-run
    var courierName: String
    /// True right after a reassignment, drives the "new rider" treatment
    var riderReassigned: Bool
  }

  /// Order identifier shown on the card, the only truly immutable fact
  var orderId: String
}
</code></pre>
<p>Moving one field touched twelve files, because the contract is duplicated at every layer that has to encode or decode it:</p>
<pre><code class="language-text">9  DeliveryNotifier.kt          Android notification builder
3  DroptrackLive.types.ts       the TypeScript contract
3  DroptrackLiveModule.swift    the Swift bridge record
3  App.tsx                      React state and the push handler
2  DeliveryLiveActivity.swift   the widget's SwiftUI
2  DeliveryAttributes.swift     the module's copy
2  DeliveryAttributes.swift     the widget's copy (yes, twice)
2  DroptrackLiveModule.kt       the Kotlin bridge record
1  DroptrackFcmService.kt, dispatch-server.mjs, push-update.mjs,
   apns.test.mjs, delivery.ts
</code></pre>
<p>The asymmetry here is what matters. A dynamic field you never change costs you a few bytes in every push. A static field you need to change costs you the entire stack. <strong>When in doubt, put the field in</strong> <code>ContentState</code><strong>.</strong> In the end, only <code>orderId</code> earned its place in the static half.</p>
<h3 id="heading-the-identical-copy-trap">The Identical-Copy Trap</h3>
<p>The widget extension is a separate binary. It's a different process, launched by the system, that doesn't link your app's code. Your app target compiles the module's <code>DeliveryAttributes.swift</code>. The widget target compiles its own. Two compilations produce two independent types that merely share a name.</p>
<p>At runtime, ActivityKit has to connect them. Your app says "start an activity of type <code>DeliveryAttributes</code>", and the system asks the widget whether it has a <a href="https://developer.apple.com/documentation/widgetkit"><code>WidgetConfiguration</code></a> for a type by that name whose <code>ContentState</code> decodes this data. The match is structural, on the type's name and its <code>Codable</code> shape, performed by a system daemon, at runtime, across a process boundary.</p>
<p>Now rename <code>courierName</code> to <code>riderName</code> in one copy and forget the other. The app compiles. The widget compiles. Both are internally consistent. <code>Activity.request()</code> succeeds and returns an id. But when the system hands the encoded state to the widget, the widget's decoder looks for <code>riderName</code>, doesn't find it, and throws inside a daemon in another process, where your breakpoints and your log statements don't exist.</p>
<p>The card never appears. No crash, no warning, no console line, and <code>Activity.activities</code> still lists the activity as running. You'll spend an hour on your SwiftUI layout, and the layout was never the problem.</p>
<p>Two things follow. A shared source file doesn't save you, because the module compiles as its own CocoaPod rather than into the app target, so no build phase naturally covers both. And since the compiler will never catch this, the check has to be external:</p>
<pre><code class="language-shell">diff modules/droptrack-live/ios/DeliveryAttributes.swift \
     targets/widgets/DeliveryAttributes.swift
</code></pre>
<p>If that command ever prints anything, your Live Activity is already broken. It costs nothing to wire into a pre-commit hook, and it's the single highest-value guardrail in this project.</p>
<h2 id="heading-how-to-build-the-widget-in-swiftui">How to Build the Widget in SwiftUI</h2>
<p>The widget declares one <a href="https://developer.apple.com/documentation/widgetkit"><code>Widget</code></a> with two presentations: the lock-screen card and the Dynamic Island.</p>
<p>Before reading the code, hold one idea in your head: <strong>these closures are a pure function from state to view.</strong> The widget extension isn't a running program. The system launches the process, calls your closure with the current <code>ContentState</code>, keeps the rendered result, and kills the process.</p>
<p>When a push delivers a new state, it runs the closure again. There's no <code>@State</code>, no timer, no network call, and no <code>onAppear</code> doing work. That's also why widget extensions have no network access: there's nobody home to make a request.</p>
<p>Two things about the type signature before the code. <code>Widget</code> is a WidgetKit protocol, not a SwiftUI <code>View</code>, and <code>body</code> returns <code>some WidgetConfiguration</code>, not <code>some View</code>. You're not describing pixels, you're declaring what kind of widget this is.</p>
<p>And the <code>for:</code> argument on line 3 is the binding point from the previous section: this is the exact spot where the app's <code>DeliveryAttributes</code> and the widget's copy are matched by name and <code>Codable</code> shape, so it's where the identical-copy trap either works or silently fails.</p>
<pre><code class="language-swift">struct DeliveryLiveActivity: Widget {
  var body: some WidgetConfiguration {
    ActivityConfiguration(for: DeliveryAttributes.self) { context in
      // Closure 1 of 2: the lock screen and banner.
      DeliveryCardView(context: context)
        .activityBackgroundTint(Color(red: 0.07, green: 0.07, blue: 0.12))
        .activitySystemActionForegroundColor(.white)

    } dynamicIsland: { context in          // closure 2 of 2, labelled
      DynamicIsland {
        // Expanded (long-press): four named slots, arranged around the cutout.
        DynamicIslandExpandedRegion(.leading) {
          Image(systemName: "bicycle").foregroundStyle(brandOrange)
        }
        DynamicIslandExpandedRegion(.trailing) {
          Text(context.state.eta, style: .time).font(.callout.bold())
        }
        DynamicIslandExpandedRegion(.center) {
          Text(context.state.status).font(.callout.weight(.semibold)).lineLimit(1)
        }
        DynamicIslandExpandedRegion(.bottom) {
          VStack(spacing: 4) {
            SegmentedProgressBar(progress: context.state.progress)
            HStack {
              CourierLabel(state: context.state, compact: true)
              Spacer()
              Text(stopsLabel(context.state.stopsRemaining))
            }
            .font(.caption2).foregroundStyle(.secondary)
          }
        }
      } compactLeading: {                   // the default pill, left of the cutout
        Image(systemName: "bicycle").foregroundStyle(brandOrange)
      } compactTrailing: {                  // ...and right of it
        Text("\(Int(context.state.progress * 100))%").font(.caption2.bold())
      } minimal: {                          // when another app shares the island
        Image(systemName: "bicycle").foregroundStyle(brandOrange)
      }
      .keylineTint(brandOrange)
    }
  }
}
</code></pre>
<p>That's one state object rendered four ways, and a few of the choices are load-bearing:</p>
<ul>
<li><p><strong>The two closures:</strong> <code>ActivityConfiguration</code> takes a content closure and a <code>dynamicIsland:</code> closure. The bare-then-labelled form is Swift's multiple-trailing-closure syntax, not two separate statements. The first is the lock-screen and banner card, and the second is every Dynamic Island form.</p>
</li>
<li><p><code>context</code><strong>:</strong> Each closure receives <code>context.state</code>, the dynamic <code>ContentState</code> replaced on every update, and <code>context.attributes</code>, the static half, read exactly once, for the order id on the card.</p>
</li>
<li><p><strong>The two</strong> <code>.activity…</code> <strong>modifiers:</strong> These are ActivityKit-specific, not general SwiftUI. You don't draw the system's swipe-to-end chrome, so all you can do is tint it.</p>
</li>
<li><p><code>eta, style: .time</code> renders a localized clock time such as "5:42 PM", honouring the user's 12- or 24-hour setting. It's not a countdown. A ticking countdown would use <code>style: .timer</code>, which the system animates with no push at all, which is how a timer-style activity stays live with zero server traffic.</p>
</li>
<li><p><code>lineLimit(1)</code> isn't decoration. The centre slot is a few dozen points wide, and an unbounded status string wraps and wrecks the layout. The bar lives in <code>.bottom</code> for the opposite reason: it's the only region wide enough.</p>
</li>
<li><p><code>.font</code> <strong>and</strong> <code>.foregroundStyle</code> <strong>on the</strong> <code>HStack</code> apply to both children through SwiftUI's environment, and a child's explicit modifier still wins over the inherited one, which is how <code>CourierLabel</code> overrides the colour on a reassignment.</p>
</li>
<li><p><code>Int(progress * 100)</code> truncates rather than rounds, so 0.999 would render as "99%". The scripted steps end on exactly 1.0 so it lands on 100, but server-computed progress would want <code>.rounded()</code>.</p>
</li>
<li><p><code>minimal</code> is the presentation people forget. When another app's activity is also running, the island shrinks yours to a single circle. Skip it and your activity looks broken whenever a timer is going. <code>keylineTint</code> sets the glow the system draws around the whole island on update.</p>
</li>
</ul>
<h3 id="heading-the-segmented-progress-bar">The Segmented Progress Bar</h3>
<p>The capsule bar is an <code>HStack</code> of rounded rectangles. <code>ContentState</code> carries <code>progress</code> as a double rather than a step index, so the widget derives how many segments to fill.</p>
<pre><code class="language-swift">private struct SegmentedProgressBar: View {
  let progress: Double
  var segments: Int = 7   // one capsule per delivery step

  // ContentState carries a 0...1 fraction, never a step index, so the bar has
  // to work backwards to a segment count. Keeping the wire format numeric means
  // the widget never needs to know what the seven steps are called.
  private var filled: Int {
    // Clamp first: a server that sends 1.4 must not paint 10 of 7 segments.
    let clamped = min(max(progress, 0), 1)
    // .rounded(.up) so a step that has merely BEGUN already lights its capsule.
    // progress 0.05 * 7 = 0.35 -&gt; ceil -&gt; 1 segment lit, not 0.
    // The outer min() guards the exact 1.0 case against a 8th segment.
    return min(segments, Int((clamped * Double(segments)).rounded(.up)))
  }

  var body: some View {
    HStack(spacing: 5) {
      // ForEach over a constant range, so `id: \.self` on the index is safe.
      ForEach(0..&lt;segments, id: \.self) { i in
        RoundedRectangle(cornerRadius: 3, style: .continuous)
          // The only stateful decision in the whole view: lit, or track colour.
          .fill(i &lt; filled ? brandOrange : trackGray)
          // Fixed height, no width: the HStack divides the width evenly, so the
          // same view fits the wide lock-screen card and the narrow island.
          .frame(height: 7)
      }
    }
  }
}
</code></pre>
<p>With progress values of 0.05, 0.15, 0.35, 0.55, 0.7, 0.85, 0.95, and 1.0, rounding <code>progress * 7</code> upward fills one through seven segments in lockstep with the steps. The same view renders on the lock-screen card and inside the expanded island, so the component reads as one thing across surfaces.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c84fbe8c8c80534346db05/539a1b65-6fdf-440a-8e9b-0ea3a02c918a.png" alt="DropTrack lock-screen card with the segmented progress bar" style="display:block;margin:0 auto" width="560" height="1213" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/66c84fbe8c8c80534346db05/ef33b5c6-bab4-4df5-9753-ed032256ece2.png" alt="DropTrack compact Dynamic Island pill" style="display:block;margin:0 auto" width="640" height="110" loading="lazy">

<h3 id="heading-the-reassignment-treatment">The Reassignment Treatment</h3>
<p>Reassignment is an in-place update: same activity id, same step, new <code>courierName</code>, and <code>riderReassigned</code> set to true. The courier row swaps its icon and copy.</p>
<pre><code class="language-swift">// Shared by the lock-screen card and the expanded island, so a reassignment
// reads identically wherever the user happens to be looking.
private struct CourierLabel: View {
  let state: DeliveryAttributes.ContentState
  let compact: Bool   // true inside the island, where horizontal space is scarce

  var body: some View {
    // Both branches are driven purely by ContentState. No local state, because
    // the widget process does not live long enough to hold any.
    if state.riderReassigned {
      Label(
        // The island cannot fit "New rider · Tunde", so it drops the prefix and
        // leans on the swap icon plus the orange tint to carry the meaning.
        compact ? state.courierName : "New rider · \(state.courierName)",
        systemImage: "arrow.triangle.2.circlepath"
      )
      // An explicit style on the child overrides the .secondary the parent
      // HStack pushed down through the environment. This is what makes the
      // swapped rider the one thing on the card that draws the eye.
      .foregroundStyle(brandOrange).bold()
    } else {
      // Steady state: inherits the parent's caption font and secondary colour.
      Label(state.courierName, systemImage: "bicycle")
    }
  }
}
</code></pre>
<p>Advancing a step retires the badge. That's the whole feature, and the reason <code>courierName</code> had to leave the static half.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c84fbe8c8c80534346db05/38944dd0-0875-4b92-b1cb-16783123848c.png" alt="DropTrack rider-reassignment treatment on the lock screen" style="display:block;margin:0 auto" width="640" height="289" loading="lazy">

<h2 id="heading-how-to-bridge-activitykit-to-javascript">How to Bridge ActivityKit to JavaScript</h2>
<p>Expo's <a href="https://docs.expo.dev/modules/module-api/"><code>Record</code></a> type gives you type-safe bridging. Expo validates the JavaScript object against these fields before your function body runs.</p>
<pre><code class="language-swift">// A Record is Expo's typed bridge struct. Every @Field is decoded out of the
// JS object BEFORE your function body runs, so a missing or wrong-typed key
// fails at the boundary with a clear error rather than deep inside Swift.
// The defaults are what a field falls back to when JS omits it.
struct DeliveryStateRecord: Record {
  @Field var status: String = ""
  @Field var progress: Double = 0
  @Field var etaEpochMillis: Double = 0   // Double, not Int: JS numbers are f64,
                                          // and epoch ms overflows Int32.
  @Field var stopsRemaining: Int = 0
  @Field var courierName: String = ""
  @Field var riderReassigned: Bool = false

  // The bridge type and the ActivityKit type are deliberately NOT the same
  // struct. This function is the single place the two vocabularies meet.
  @available(iOS 16.2, *)
  func toContentState() -&gt; DeliveryAttributes.ContentState {
    DeliveryAttributes.ContentState(
      status: status,
      // Clamp at the boundary, so no downstream view has to defend itself.
      progress: min(max(progress, 0), 1),
      // JS speaks Unix milliseconds; Foundation.Date wants seconds. Divide once,
      // here, rather than scattering /1000 through the codebase.
      eta: Date(timeIntervalSince1970: etaEpochMillis / 1000),
      stopsRemaining: stopsRemaining,
      courierName: courierName,
      riderReassigned: riderReassigned
    )
  }
}
</code></pre>
<p>And the module itself:</p>
<pre><code class="language-swift">AsyncFunction("startDelivery") { (info: DeliveryInfoRecord, state: DeliveryStateRecord) -&gt; String in
  // Runtime OS guard: the module compiles against older targets, so this is
  // what stops it calling into a framework that isn't there.
  guard #available(iOS 16.2, *) else { throw LiveActivityUnsupportedException() }

  let activity = try Activity.request(
    attributes: DeliveryAttributes(orderId: info.orderId),                  // static half, set once
    content: ActivityContent(state: state.toContentState(), staleDate: nil), // dynamic half
    pushType: .token                                                        // ask APNs for a token
  )
  return activity.id   // the only handle JS gets, and the one it forgets on reload
}

AsyncFunction("updateDelivery") { (activityId: String, state: DeliveryStateRecord) in
  let activity = try Self.findActivity(id: activityId)
  // .update() replaces the whole ContentState. There is no partial-patch API.
  await activity.update(ActivityContent(state: state.toContentState(), staleDate: nil))
}
</code></pre>
<p>A few notes on the arguments, so they stay out of the code. <code>AsyncFunction</code> (rather than <code>Function</code>) is what makes the JS side a <code>Promise</code>, and Expo infers the rest of the signature from the parameter types.</p>
<p><code>staleDate: nil</code> means the card is never marked stale. Pass a date and, once it passes, the system flips the activity's state so your widget can read <code>context.isStale</code> and render a degraded view, but it doesn't grey the card out for you.</p>
<p>And <code>pushType: .token</code> is what asks APNs for a per-activity token, where passing <code>nil</code> instead gives you a purely local activity that no server can update.</p>
<p>Notice <code>findActivity</code>. Activities are looked up fresh by id on every call rather than cached in a property, because activity handles live inside <a href="https://developer.apple.com/documentation/activitykit/activity">ActivityKit</a>, not in your process. That distinction matters enormously, as the next section shows.</p>
<pre><code class="language-swift">@available(iOS 16.2, *)
private static func findActivity(id: String) throws -&gt; Activity&lt;DeliveryAttributes&gt; {
  // `Activity.activities` is a live, system-owned list. It survives an app
  // relaunch, a JS reload, and a process kill. Caching a handle in a Swift
  // property would not, which is precisely why this lookup runs every time.
  guard let activity = Activity&lt;DeliveryAttributes&gt;.activities.first(where: { $0.id == id }) else {
    // Reached when the user swiped the card away, or the activity aged out.
    throw ActivityNotFoundException(id)
  }
  return activity
}
</code></pre>
<h2 id="heading-four-ios-gotchas-that-cost-me-an-evening-each">Four iOS Gotchas That Cost Me an Evening Each</h2>
<p>Each of these wasted an evening because the failure looked like a bug in my code when it was really a quirk of the platform. They share a shape, so I've written each one the same way: the symptom you see, why it happens, and what to do about it.</p>
<h3 id="heading-gotcha-1-the-compact-island-looks-broken">Gotcha 1: The Compact Island Looks Broken</h3>
<p>You build the compact Dynamic Island, run the app, and it never appears.</p>
<p>This happens because the compact presentation is hidden while your own app is in the foreground. This is by design, but nothing tells you so, and I spent twenty minutes certain my layout was wrong.</p>
<p>To fix this, lock the device, or switch to another app, before you judge whether the island works. There's nothing to fix in the code.</p>
<h3 id="heading-gotcha-2-an-old-activity-renders-nothing-after-a-rebuild">Gotcha 2: An Old Activity Renders Nothing After a Rebuild</h3>
<p>You change the widget, rebuild, and an activity that was already running goes blank. No error appears anywhere.</p>
<p>This happens when an activity is tied to the exact build that started it. Once you rebuild the widget extension, the running activity no longer matches the code on the device.</p>
<p>To fix this, after any change to the widget extension, end the stale activity and start a fresh one. Don't expect a warning to remind you.</p>
<h3 id="heading-gotcha-3-reinstalling-can-jam-the-system-daemon">Gotcha 3: Reinstalling Can Jam the System Daemon</h3>
<p>Right after <code>simctl install</code>, your first activity refuses to render. <code>Activity.activities</code> says it's active, yet the screen shows nothing.</p>
<p>This happens because Live Activities are drawn by a background system process called <code>chronod</code>. A reinstall can leave it wedged: in my case it threw <code>widgetDescriptorNotFound</code> and logged internal errors, and no amount of ending and restarting the activity cleared it.</p>
<p>This is a fault in the daemon, not in your app, which is why the usual checks mislead you. Listing the extension with <code>pluginkit -m</code> shows it present and healthy while the daemon behind it is stuck.</p>
<p>To fix this, reboot the simulator with <code>simctl shutdown &amp;&amp; boot</code>, then start a fresh activity.</p>
<h3 id="heading-gotcha-4-tapping-the-card-opens-an-app-that-forgot-everything">Gotcha 4: Tapping the Card Opens an App That Forgot Everything</h3>
<p>This is the subtle one, the most visible to users, and the reason for the next two code samples.</p>
<p>The user taps the live card to open your app. The app launches into an empty screen that insists nothing is being tracked, every control disabled, while the card sits right there on the lock screen still updating.</p>
<p>This happens because tapping the card cold-starts the app, which means your JavaScript begins from scratch. The activity id lived in React state, and that state died with the previous process.</p>
<p>The activity itself is completely fine: <code>Activity.activities</code> still lists it, and an APNs push to it still returns <code>200</code>. (A dead activity returns <code>410</code>, which makes a handy liveness probe.) The app simply forgot the id it needs to reconnect.</p>
<p>To fix this, stop treating your in-memory id as the source of truth, and ask the system what is running on launch. Expose the live activities to JavaScript.</p>
<pre><code class="language-swift">AsyncFunction("getRunningActivities") { () -&gt; [[String: Any]] in
  guard #available(iOS 16.2, *) else { return [] }

  // The system's list, populated even on a brand-new process. That is the point.
  return Activity&lt;DeliveryAttributes&gt;.activities.map { activity in
    self.observePushToken(of: activity)   // resubscribe: see note below
    let state = activity.content.state    // the live state, kept current by push
    return [
      "activityId": activity.id,                // the handle JS lost
      "orderId": activity.attributes.orderId,   // the static half
      "status": state.status,
      "progress": state.progress,
      "courierName": state.courierName,
      "riderReassigned": state.riderReassigned,
    ]
  }
}
</code></pre>
<p>That <code>observePushToken</code> call is the line everyone misses. The <code>for await</code> loop watching this activity's push token died with the previous process, so without resubscribing here the card recovers. But the server never learns the token again after a rotation, and pushes quietly stop landing.</p>
<p>Then rehydrate on mount, and again on every foreground, because a push that lands while you're backgrounded updates the widget without running your JavaScript at all. React Native's <a href="https://reactnative.dev/docs/appstate"><code>AppState</code></a> gives you the hook.</p>
<pre><code class="language-typescript">useEffect(() =&gt; {
  const sync = () =&gt; {
    void DroptrackLive.getRunningActivities().then((running) =&gt; {
      const activity = running[0];
      if (!activity) return;   // nothing running: leave the empty state alone

      setActivityId(activity.activityId);   // this line re-enables the whole UI
      setRider({ name: activity.courierName, justReassigned: activity.riderReassigned });

      // The card carries a status string, not a step index. Guard the -1.
      const index = STEPS.findIndex((s) =&gt; s.status === activity.status);
      if (index &gt;= 0) setStepIndex(index);
    });
  };

  sync();   // case 1: cold start (e.g. the user tapped the card and we launched)
  const sub = AppState.addEventListener("change", (s) =&gt; {
    if (s === "active") sync();   // case 2: resume from background
  });
  return () =&gt; sub.remove();
}, []);
</code></pre>
<p>The whole tracking interface is gated on <code>activityId</code>, so restoring it is what turns the app back on. Miss that one line and the app looks dead while the card is live on the lock screen.</p>
<p>The two call sites cover the two ways state goes stale. <code>sync()</code> on mount handles a cold start, and the <code>AppState</code> listener handles a resume, because the mount effect won't re-run and a push may have advanced the card while you were backgrounded without executing any JavaScript. The empty dependency array means this wiring is set up once, for the app's lifetime.</p>
<p>That last gotcha generalises into the sentence I wish I'd read first: <strong>Live Activities are owned by the system, not by your process.</strong> The API hands you an activity id and it's easy to assume the id is yours to keep. It's not. The activity outlives the variable, and the moment a user is most likely to open your app, by tapping the live card, is precisely the moment your in-memory copy of that id doesn't exist.</p>
<h2 id="heading-how-to-drive-ios-from-a-server-with-apns">How to Drive iOS From a Server With APNs</h2>
<p>Here's the payoff: updating the card while the app is fully force-quit. The API surface is almost insultingly small. You change one argument. Apple documents the flow in <a href="https://developer.apple.com/documentation/activitykit/starting-and-updating-live-activities-with-activitykit-push-notifications">Starting and updating Live Activities with ActivityKit push notifications</a>.</p>
<pre><code class="language-swift">let activity = try Activity.request(
  attributes: attributes,
  content: ActivityContent(state: state.toContentState(), staleDate: nil),
  pushType: .token   // this was `nil`. That is the entire feature.
)

Task {
  for await tokenData in activity.pushTokenUpdates {
    let token = tokenData.map { String(format: "%02x", $0) }.joined()   // raw Data -&gt; hex
    NSLog("[DropTrack] push token for %@: %@", activity.id, token)       // NSLog, so devicectl sees it
    self.sendEvent("onPushTokenReceived", ["activityId": activity.id, "token": token])
  }
}
</code></pre>
<p><code>pushTokenUpdates</code> is an <code>AsyncSequence</code>, not a one-shot getter, which is why this is a <code>for await</code> loop and not a single read. Reading <code>activity.pushToken</code> right after <code>request()</code> reliably returns nil, because the token hasn't arrived from APNs yet, and it can also rotate mid-flight.</p>
<p>The token arrives as raw <code>Data</code>, so it's hexed before use. It's logged with <code>NSLog</code> rather than <code>print</code> because a standalone Release build has no Metro to receive <code>console.log</code>, and <code>NSLog</code> is what <code>devicectl --console</code> surfaces. And the token is per-activity, so it's keyed by activity id when handed to JS rather than stored globally.</p>
<p>Everything after that is plumbing, and two pieces of it fail without a single error message.</p>
<p><code>pushType: .token</code> <strong>does nothing without the</strong> <a href="https://developer.apple.com/documentation/bundleresources/entitlements/aps-environment"><code>aps-environment</code></a> <strong>entitlement.</strong> <code>Activity.request</code> still succeeds. Local updates still work. The token stream simply never yields. Nothing logs. Add the entitlement in <code>app.json</code>:</p>
<pre><code class="language-json">"ios": {
  "entitlements": {
    "aps-environment": "development",
    "com.apple.security.application-groups": ["group.com.fasarticle.droptrack"]
  }
}
</code></pre>
<p>Two notes on those values, since <code>app.json</code> can't carry comments. <code>aps-environment</code> set to <code>development</code> means your tokens are only valid against <code>api.sandbox.push.apple.com</code>. A shipping build needs <code>production</code>, and mixing the two produces an afternoon of <code>BadDeviceToken</code>. The application group is the shared container between the app and the widget extension, and both targets must carry the identical group id or shared reads silently return nothing.</p>
<p>Then verify that the entitlement survived signing, because an entitlement in your config isn't the same as an entitlement in your binary:</p>
<pre><code class="language-shell"># Reads the entitlements actually baked into the signed .app.
# If aps-environment is absent here, the token stream will never yield,
# no matter what app.json says.
codesign -d --entitlements - --xml Build/Products/Release-iphoneos/DropTrack.app
</code></pre>
<p><strong>The push token is per-activity, not per-device.</strong> It arrives asynchronously after <code>request()</code> returns, and the system can rotate it mid-flight. Consume the <code>pushTokenUpdates</code> async sequence. A single read of <code>activity.pushToken</code> immediately after <code>request()</code> is reliably nil. Every <code>startDelivery</code> mints a brand-new token. I verified that by starting two activities and comparing the hex.</p>
<h2 id="heading-how-to-write-an-apns-client-from-scratch">How to Write an APNs Client From Scratch</h2>
<p>You don't need a library. Two Node.js built-ins and about sixty lines will do it.</p>
<p>Sending one push comes down to three steps. First, sign a token that proves the request is really from you. Second, open a connection to Apple and attach a precise set of headers. Third, send the state you want the card to show. The rest of this section walks each step, then covers the two ways it fails silently.</p>
<p>One thing to get out of the way first: APNs speaks HTTP/2 only, so <code>fetch()</code> can't reach it. You need Node's <a href="https://nodejs.org/api/http2.html"><code>node:http2</code></a> module, which is why this is written by hand rather than as a <code>fetch</code> call.</p>
<h3 id="heading-step-1-sign-a-token-that-proves-who-you-are">Step 1: Sign a Token That Proves Who You Are</h3>
<p>APNs won't accept a push until you prove you own the app. You do that with an ES256 <a href="https://datatracker.ietf.org/doc/html/rfc7519">JSON Web Token</a>, signed by the <code>.p8</code> key you download from Apple, following <a href="https://developer.apple.com/documentation/usernotifications/establishing-a-token-based-connection-to-apns">Establishing a token-based connection to APNs</a>. A JWT is just three base64url chunks joined by dots: a header, a claims object, and a signature over the first two.</p>
<pre><code class="language-javascript">import { createPrivateKey, sign } from "node:crypto";

const b64url = (buf) =&gt; Buffer.from(buf).toString("base64url");   // JWT uses base64url, not base64

export function mintJWT({ keyPath, keyId, teamId }) {
  const header = b64url(JSON.stringify({ alg: "ES256", kid: keyId }));   // kid: the 10-char key id
  const claims = b64url(JSON.stringify({ iss: teamId, iat: Math.floor(Date.now() / 1000) }));
  const signingInput = `${header}.${claims}`;   // a JWT signs header AND claims joined

  const signature = sign("sha256", Buffer.from(signingInput), {
    key: createPrivateKey(readFileSync(keyPath, "utf8")),
    dsaEncoding: "ieee-p1363",   // THE line. The default (DER) fails silently. See below.
  });

  return `${signingInput}.${b64url(signature)}`;
}
</code></pre>
<p>That one <code>dsaEncoding</code> line is the whole reason to write this by hand rather than trust a snippet off the internet.</p>
<p>Node signs in DER format by default, which wraps the signature's two halves in an envelope, but the JWT standard (<a href="https://datatracker.ietf.org/doc/html/rfc7518">RFC 7518</a>) wants those two halves raw and joined. Use the wrong one and APNs answers <code>403 InvalidProviderToken</code>, which reads like your key is bad and sends you re-downloading the <code>.p8</code> for an hour, when the key was fine all along.</p>
<p>Two smaller details. <code>kid</code> is the ten-character key id (it is in the <code>.p8</code> filename), and it tells APNs which of your registered keys should check the signature. And the <code>iat</code> timestamp can't be more than an hour old, per Apple, which also asks you to refresh it no more than once every 20 minutes, so cache the token instead of minting a fresh one for every push.</p>
<h3 id="heading-step-2-open-the-connection-and-send-the-push">Step 2: Open the Connection and Send the Push</h3>
<p>With the token in hand, open an HTTP/2 session to the APNs host and make one <code>POST</code>. The headers have to be exact, and the one people get wrong is the topic: it's your bundle id with <code>.push-type.liveactivity</code> appended, not the bundle id on its own.</p>
<pre><code class="language-javascript">// Pseudo-headers (":method", ":path") are HTTP/2's way of encoding the request
// line. `client` here is an http2 session opened against the APNs host.
const req = client.request({
  ":method": "POST",
  // The device token in the path is the PER-ACTIVITY token, not a device token
  // from UNUserNotificationCenter. Those are different values entirely.
  ":path": `/3/device/${token}`,
  authorization: `bearer ${jwt}`,   // lowercase "bearer" is what APNs expects

  // Live Activity pushes get a SUFFIXED topic. Send the bare bundle id and
  // APNs rejects the request outright.
  "apns-topic": `${bundleId}.push-type.liveactivity`,
  "apns-push-type": "liveactivity", // must match the topic suffix
  "apns-priority": "10",            // 10 = deliver immediately, 5 = opportunistic
  "apns-expiration": "0",           // 0 = do not retry a failed push
});

req.end(JSON.stringify({
  aps: {
    // Ordering guard, in SECONDS. APNs discards a push whose timestamp is older
    // than the last one it applied, so out-of-order retries cannot rewind the card.
    timestamp: Math.floor(Date.now() / 1000),
    event: "update",              // "update" mutates the card, "end" finishes it
    // Decoded straight into the widget's Codable ContentState. Field names and
    // types must match that struct EXACTLY. See the eta trap below.
    "content-state": contentState,
  },
}));
</code></pre>
<p>The body is small on purpose. <code>timestamp</code> is an ordering guard in seconds: APNs drops any push older than the last one it applied, so a delayed retry can't rewind the card. <code>event</code> is <code>"update"</code> to change the card or <code>"end"</code> to finish the activity, and there's no <code>"start"</code> here because push-to-start is a separate token that needs iOS 17.2. Everything else lives in <code>content-state</code>, which the widget decodes directly, and that's where the second silent failure hides, below.</p>
<h3 id="heading-how-to-test-your-auth-without-a-phone">How to Test Your Auth Without a Phone</h3>
<p>This one trick saved me the most time, so do it before you involve a device at all. Send a push to the sandbox using a deliberately fake device token, and read the <a href="https://developer.apple.com/documentation/usernotifications/handling-notification-responses-from-apns">response code</a> APNs gives back.</p>
<ul>
<li><p><code>400 BadDeviceToken</code> is the good outcome. It means your key, key id, team id, and JWT encoding are all correct, and APNs got far enough to look up the token and simply not find it. Your auth works.</p>
</li>
<li><p><code>403 InvalidProviderToken</code> means the token itself is wrong, so the problem is your key or your signing, not the device.</p>
</li>
</ul>
<p>That single request separates "my auth is broken" from "my token is broken" with no phone in the room. On this project it returned <code>400</code> on the first try, which told me the auth was solid and I should look elsewhere for the real bug. That was exactly the reassurance I needed.</p>
<h3 id="heading-the-eta-trap-a-200-that-renders-nothing">The <code>eta</code> Trap: a 200 That Renders Nothing</h3>
<p>This is the most instructive bug in the project, and it hit the moment a second code path reused a shared helper.</p>
<p>The same delivery data takes two different routes to the widget, and the routes expect two different shapes. The native module path carries <code>etaEpochMillis</code> (Unix milliseconds), and Swift converts it into a <code>Date</code> on the way through. The push path skips Swift entirely: the JSON <code>content-state</code> is decoded directly by the widget's <code>Codable</code> struct, which expects a field literally named <code>eta</code>, holding seconds since 2001 (Apple's <a href="https://developer.apple.com/documentation/foundation/date">reference date</a>), and knows nothing about <code>etaEpochMillis</code>.</p>
<p>So if you reuse the native-bridge object as a push payload, APNs returns <code>200</code>, iOS quietly throws the update away because it won't decode, and nothing is logged anywhere. The fix is to translate at the boundary, converting to the widget's shape only when building a push.</p>
<pre><code class="language-javascript">const APPLE_EPOCH_OFFSET = 978_307_200;   // seconds between 1970 and 2001

// The shape the WIDGET decodes, not the shape the native bridge takes.
function toContentState({ etaEpochMillis, ...rest }) {
  return {
    ...rest,
    eta: Math.floor(etaEpochMillis / 1000) - APPLE_EPOCH_OFFSET,   // ms-&gt;s, then rebase to 2001
  };
}
</code></pre>
<p>Two details make this work. <code>Foundation.Date</code>'s reference point is 2001-01-01, not the Unix epoch, and Swift's <code>Codable</code> encodes a <code>Date</code> as seconds since that date, which is why the offset is subtracted. And destructuring <code>etaEpochMillis</code> out of <code>rest</code> is what drops the wrong key: leave it in and the widget's <code>Codable</code> init sees an unexpected field next to a missing <code>eta</code>, fails to decode, and the update vanishes after a 200.</p>
<p>The lesson generalises past the <code>eta</code> field. <strong>A</strong> <code>200</code> <strong>from APNs means only that Apple accepted your bytes, not that the card changed.</strong> Any content-state that fails to decode is discarded with no error on any surface, so confirm on the lock screen every time rather than trusting the status code.</p>
<p>Two last environment traps to note. Development-signed builds must talk to <code>api.sandbox.push.apple.com</code>, because the production host returns <code>400 BadDeviceToken</code> for a sandbox token, which looks exactly like a malformed token and sends you debugging the wrong layer. And if you push frequently, add <code>NSSupportsLiveActivitiesFrequentUpdates</code> to your <code>Info.plist</code>, or the system budgets and drops your rapid pushes.</p>
<h2 id="heading-how-to-test-on-real-hardware">How to Test on Real Hardware</h2>
<p>The simulator will lie to you about push, so at some point you need a real phone. Four things can trip you up there that the documentation never mentions. Here is each one with the fix.</p>
<h3 id="heading-build-in-release-not-debug">Build in Release, not Debug</h3>
<p>A Debug build expects to download its JavaScript from Metro over your local network. The build script bakes your Mac's network address into the app, so the moment the phone is on a different network, or simply not tethered to your Mac, it shows a red screen reading <code>No script URL provided</code>. There's no <code>adb reverse</code> equivalent on iOS to paper over this.</p>
<p>Build with <code>--configuration Release</code> instead, which embeds the JavaScript bundle inside the app. That's the more honest test anyway, because a Release build is the only way to fully force-quit the app and prove the push, not a live Metro connection, is doing the work.</p>
<h3 id="heading-read-logs-with-devicectl-not-consolelog">Read Logs with <code>devicectl</code>, Not <code>console.log</code></h3>
<p>Once you drop Metro, <code>console.log</code> has nowhere to go. To read something like a push token off the device, you need the app's real standard output, and <code>NSLog</code> in the Swift code plus <code>devicectl</code> on the Mac is the reliable way to get it.</p>
<pre><code class="language-shell"># --console            stream the app's stdout/stderr back to this terminal
# --terminate-existing kill a running copy first, so we catch launch-time logs
# --device             the COREDEVICE id (see the identifier trap below)
xcrun devicectl device process launch --console --terminate-existing \
  --device &lt;coredevice-id&gt; com.fasarticle.droptrack
# → [DropTrack] push token for 3095ACA0-...: 80875cb137590013a4c9...
#   ^ the hex string the dispatch server scrapes and pushes to
</code></pre>
<h3 id="heading-know-which-of-the-two-device-identifiers-you-need">Know Which of the Two Device Identifiers You Need</h3>
<p>The same phone has two identifiers, and the tools disagree about which they want. <code>expo run:ios --device</code> wants the hardware UDID, listed by <code>xcrun xctrace list devices</code>. The <code>devicectl</code> command above wants the CoreDevice identifier, listed by <code>xcrun devicectl list devices</code>.</p>
<p>Pass one where the other belongs and you get <code>No device UDID or name matching</code>, which reads as though the phone is unplugged. It's not, so check that you copied the right kind of id before you check the cable.</p>
<h3 id="heading-handle-cold-start-deep-links-not-just-live-ones">Handle Cold-start Deep Links, Not Just Live Ones</h3>
<p>If your test driver opens the app with a deep link, note that <code>devicectl ... --payload-url</code> cold-starts the app, so the link arrives through <a href="https://reactnative.dev/docs/linking"><code>Linking.getInitialURL()</code></a> rather than the <code>url</code> event that the simulator's <code>simctl openurl</code> fires. A driver that listens only for the <code>url</code> event will appear to do nothing on a device. Handle both entry points.</p>
<p>With all four handled and the app confirmed fully quit, I drove three pushes through it (a status change, a rider reassignment, and an "arriving now"). Each one rendered on the lock screen and in the Dynamic Island.</p>
<p>One bonus comes free. Sign the Mac into the same Apple ID, and Continuity mirrors the same Live Activity onto the macOS menu bar, segmented bar, and all. One APNs push, three surfaces.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c84fbe8c8c80534346db05/9aaf2fd9-07b5-49b8-a6cb-6befe06b9e15.png" alt="DropTrack Live Activity on the macOS menu bar via Continuity" style="display:block;margin:0 auto" width="1200" height="305" loading="lazy">

<h2 id="heading-why-android-has-no-activitykit">Why Android Has No ActivityKit</h2>
<p>Android has no equivalent of ActivityKit, so there's no system-owned card and no framework keeping it up to date. What you have instead is an ordinary notification that you keep re-posting.</p>
<p>The trick is the notification id. Re-post under the same id and Android replaces the existing notification in place, which reads as an update. Post under a new id and you get a second notification instead. So the three lifecycle verbs map onto plain notification calls:</p>
<ul>
<li><p><strong>start</strong> is <code>notify()</code> with a fresh id.</p>
</li>
<li><p><strong>update</strong> is <code>notify()</code> again under that same id.</p>
</li>
<li><p><strong>end</strong> is a final <code>notify()</code> with <code>ongoing = false</code>, followed by a delayed <code>cancel()</code>.</p>
</li>
</ul>
<p>That is the entire model. The catch is that everything iOS did for you (keeping the card current, recovering it after a restart, and updating it from a push) is now code you have to write.</p>
<h3 id="heading-the-trap-that-shapes-everything-android-16-is-two-releases-in-one">The Trap That Shapes Everything: Android 16 is Two Releases in One</h3>
<p>One versioning quirk shapes the whole Android side, so it's worth thirty seconds up front. "Android 16" ships as two releases that share a single API number:</p>
<table>
<thead>
<tr>
<th>Release</th>
<th>Reports</th>
<th>What it adds</th>
</tr>
</thead>
<tbody><tr>
<td>Android 16 (base)</td>
<td><code>SDK_INT == 36</code></td>
<td><a href="https://developer.android.com/reference/android/app/Notification.ProgressStyle"><code>ProgressStyle</code></a>, the segmented progress bar</td>
</tr>
<tr>
<td>Android 16 QPR1</td>
<td><code>SDK_INT == 36</code></td>
<td>the <em>promotion</em> pipeline: the status-bar chip and the elevated lock-screen slot</td>
</tr>
</tbody></table>
<p>Both report <code>36</code> because <code>Build.VERSION.SDK_INT</code>, the value your code reads to check the OS version, is a plain integer with no room for a <code>.1</code>. Two different releases, one number.</p>
<p>That collision is the entire source of the friction, and it leaves you three things to handle. None is hard once you know it is coming.</p>
<h4 id="heading-1-compile-through-the-backport-not-the-platform-classes">1. Compile through the backport, not the platform classes</h4>
<p>The promotion methods (<code>setRequestPromotedOngoing</code>, <code>setShortCriticalText</code>, and the <code>POST_PROMOTED_NOTIFICATIONS</code> permission) live only in the 36.1 SDK, and React Native 0.86 compiles against base 36. Call them on the platform classes directly and the build fails with "no such method" before the app ever runs. One dependency fixes that:</p>
<pre><code class="language-gradle">dependencies {
  // NotificationCompat backports the 36.1 promotion APIs so they compile
  // against base SDK 36. Without this, ProgressStyle, setRequestPromotedOngoing
  // and setShortCriticalText are simply not on the classpath.
  implementation 'androidx.core:core-ktx:1.17.0'
}
</code></pre>
<p>Call the <a href="https://developer.android.com/reference/androidx/core/app/NotificationCompat.Builder"><code>NotificationCompat</code></a> versions rather than the platform ones. They compile against 36, take effect on a real 36.1 device, and are ignored on older releases, so one build is safe everywhere.</p>
<h4 id="heading-2-read-sdkintfull-only-when-you-truly-need-the-minor-version">2. Read <code>SDK_INT_FULL</code> only when you truly need the minor version</h4>
<p>A normal <code>SDK_INT &gt;= 36</code> check can't tell base 36 from 36.1, since both say 36. When you genuinely need to know which release you are on, <a href="https://developer.android.com/reference/android/os/Build.VERSION#SDK_INT_FULL"><code>Build.VERSION.SDK_INT_FULL</code></a> carries the minor version, with named values in <a href="https://developer.android.com/reference/android/os/Build.VERSION_CODES_FULL"><code>Build.VERSION_CODES_FULL</code></a>. Because of rule 1 you rarely need it for the promotion request itself. You reach for it to decide what to show the user, or to guard the call in rule 3.</p>
<h4 id="heading-3-wrap-the-capability-check-so-it-cant-crash-you">3. Wrap the capability check so it can't crash you</h4>
<p><a href="https://developer.android.com/reference/android/app/NotificationManager#canPostPromotedNotifications()"><code>NotificationManager.canPostPromotedNotifications()</code></a> reports whether the device will actually honour a promotion. Its reference lists it as <a href="https://developer.android.com/reference/android/app/NotificationManager#canPostPromotedNotifications()">added in API 36</a>, so in theory it belongs to base Android 16, while the <a href="https://developer.android.com/reference/android/Manifest.permission#POST_PROMOTED_NOTIFICATIONS"><code>POST_PROMOTED_NOTIFICATIONS</code></a> permission it depends on only arrived in 36.1. On the pre-QPR build I tested, though, the call was absent at runtime and threw. A capability probe should never be the thing that crashes your app, so wrap it and default to false:</p>
<pre><code class="language-kotlin">Function("canPostPromotedNotifications") {
  // Function, not AsyncFunction: this is a cheap synchronous read, and the UI
  // wants it during the first render to decide what to disable.
  val manager = notificationManager ?: return@Function false

  // Cheapest gate first: below API 36 there is nothing to promote to.
  if (Build.VERSION.SDK_INT &lt; 36) return@Function false

  // Docs list this as added in API 36, but on the pre-QPR build I tested it was
  // absent at runtime and threw. runCatching stops a capability probe from
  // crashing the app, defaulting to false when the call is unavailable.
  return@Function runCatching { manager.canPostPromotedNotifications() }.getOrDefault(false)
}
</code></pre>
<h2 id="heading-how-to-build-the-kotlin-side">How to Build the Kotlin Side</h2>
<p>Here's the notification builder. Notice how many of these lines are promotion requirements rather than cosmetics.</p>
<pre><code class="language-kotlin">// Whole percent, clamped: a server that sends 1.4 must not become 140.
val progressPercent = (progress.coerceIn(0.0, 1.0) * 100).toInt()

val builder = NotificationCompat.Builder(ctx, CHANNEL_ID)
  .setSmallIcon(R.drawable.ic_delivery)
  .setContentTitle(status)                     // promotion REQUIRES a title
  .setContentText(courierLine(courierName, riderReassigned, stopsRemaining))
  .setSubText("Order $orderId")
  .setOngoing(ongoing)                         // promotion REQUIRES ongoing
  .setOnlyAlertOnce(true)                      // buzz once, then update silently
  .setColor(BRAND_ORANGE)                      // NOT setColorized: that disqualifies it
  .setShortCriticalText("$progressPercent%")   // text inside the status-bar chip
  .setRequestPromotedOngoing(ongoing)          // the promotion request itself

// `when` is the ETA in the header. It must be in the future (see note below).
if (etaEpochMillis &gt; System.currentTimeMillis()) {
  builder.setWhen(etaEpochMillis.toLong()).setShowWhen(true)
}

if (Build.VERSION.SDK_INT &gt;= 36) {
  val style = NotificationCompat.ProgressStyle()
    .setStyledByProgress(true)                 // colour follows the value
    .setProgress(progressPercent)
    .setProgressTrackerIcon(IconCompat.createWithResource(ctx, R.drawable.ic_delivery))
    .setProgressSegments(listOf(               // segments: spans of the bar
      NotificationCompat.ProgressStyle.Segment(100).setColor(BRAND_ORANGE)
    ))
    .setProgressPoints(listOf(                 // points: milestone dots on top
      NotificationCompat.ProgressStyle.Point(35).setColor(Color.WHITE)
    ))
  builder.setStyle(style)
} else {
  builder.setProgress(100, progressPercent, false)  // pre-16 fallback bar
}

val notification = builder.build()
if (Build.VERSION.SDK_INT &gt;= 36) {
  Log.d(TAG, "hasPromotableCharacteristics=${notification.hasPromotableCharacteristics()}")
}
manager.notify(notificationIdFor(activityId), notification)   // stable id = update in place
</code></pre>
<p>Several of those lines carry non-obvious weight:</p>
<ul>
<li><p><code>setOnlyAlertOnce(true)</code> stops each of the seven re-posts per delivery from buzzing the phone. Only the first <code>notify()</code> alerts, and the rest land silently.</p>
</li>
<li><p><code>setColor</code> <strong>versus</strong> <code>setColorized</code><strong>:</strong> <code>setColor</code> tints the notification and is fine, but <code>setColorized(true)</code> would <em>disqualify</em> it from promotion. They're not interchangeable.</p>
</li>
<li><p><code>setWhen</code> must be a future timestamp, or the update can be skipped entirely. One UI renders it as an absolute clock time, Pixel as a relative countdown.</p>
</li>
<li><p><code>ProgressStyle</code> <strong>segments versus points:</strong> <em>Segments</em> are spans of the bar (one <code>Segment(100)</code> fills the whole thing, several draw the divided look). <em>Points</em> are milestone dots painted on top at a given percent.</p>
</li>
<li><p><strong>The</strong> <code>else</code> <strong>branch:</strong> Below API 36 the compat layer has no <code>ProgressStyle</code> at all, so it falls back to the classic determinate bar.</p>
</li>
</ul>
<p>Two runtime checks are your only debugging signal, and they answer different questions:</p>
<ul>
<li><p><code>hasPromotableCharacteristics()</code>, logged after <code>build()</code>, tells you whether <em>you</em> satisfied every promotion precondition. It says nothing about the device.</p>
</li>
<li><p><code>canPostPromotedNotifications()</code> tells you whether the <em>device</em> will honour the request. The two genuinely disagree on Samsung, which is why you need both.</p>
</li>
</ul>
<p>And <code>notificationIdFor()</code> derives a stable int from the activity id, so a re-post under the same id updates the card in place. A different id would post a second notification, the classic "why do I have seven delivery cards" bug.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c84fbe8c8c80534346db05/b815c40a-9c1f-42c6-a8da-1dc6647eb7d1.png" alt="DropTrack notification with a ProgressStyle segmented bar in the shade" style="display:block;margin:0 auto" width="640" height="1386" loading="lazy">

<p><strong>Promotion eligibility is all-or-nothing, and silent.</strong> It flips true only when every condition holds: notification permission, <code>setRequestPromotedOngoing(true)</code>, ongoing, a content title, an allowed style, importance above <code>MIN</code>, and not colorized. Miss one and you get an ordinary notification with no explanation. Log <a href="https://developer.android.com/reference/android/app/Notification#hasPromotableCharacteristics()"><code>hasPromotableCharacteristics()</code></a> after every <code>build()</code>, and surface <code>canPostPromotedNotifications()</code> in your development interface. Those two booleans are the only debugging signal the platform gives you.</p>
<p>Note also that custom <code>RemoteViews</code> aren't allowed for promoted notifications. You get the <code>ProgressStyle</code> template, or you get no promotion. That's the biggest design constraint compared with iOS, where you write arbitrary SwiftUI.</p>
<p>When it does promote on a real API 36.1 build, you get the chip and the elevated lock-screen slot.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c84fbe8c8c80534346db05/d0c42fc5-3c8a-42c5-8398-cc4fc358a174.png" alt="DropTrack status-bar chip on Android 16 QPR" style="display:block;margin:0 auto" width="636" height="59" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/66c84fbe8c8c80534346db05/1583af2a-bd36-40c1-9b6b-9fca3179669f.png" alt="DropTrack promoted lock-screen placement on Android 16 QPR" style="display:block;margin:0 auto" width="640" height="1386" loading="lazy">

<p>The same APK behaves three different ways:</p>
<table>
<thead>
<tr>
<th>Runtime</th>
<th>Result</th>
</tr>
</thead>
<tbody><tr>
<td>API 36.1 (QPR)</td>
<td>Chip, promoted lock-screen slot, and segmented bar</td>
</tr>
<tr>
<td>API 36 (base)</td>
<td>Segmented bar only, no promotion</td>
</tr>
<tr>
<td>Below API 36</td>
<td>No bar at all, unless you keep the manual <code>setProgress</code> branch</td>
</tr>
</tbody></table>
<h2 id="heading-how-to-drive-android-from-a-server-with-fcm">How to Drive Android From a Server With FCM</h2>
<p>This is the deepest difference between the two platforms.</p>
<p>On iOS, APNs updates the widget through the system, and your app code never runs. On Android, there's no system-managed remote update. A data-only FCM message wakes a <a href="https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/FirebaseMessagingService"><code>FirebaseMessagingService</code></a>, and your code re-posts the notification. The app is the updater, even from the background.</p>
<pre><code class="language-kotlin">// Registered in the manifest, so Android can instantiate it WITHOUT your
// Activity, your React context, or your module ever existing.
class DroptrackFcmService : FirebaseMessagingService() {

  // Called on install, and again whenever FCM rotates the token. Never assume
  // the token you saw at startup is still valid.
  override fun onNewToken(token: String) {
    Log.i(DeliveryNotifier.TAG, "[DropTrack] fcm token: $token")
  }

  override fun onMessageReceived(message: RemoteMessage) {
    // FCM `data` is always Map&lt;String, String&gt;. Every number and boolean was
    // stringified on the way out and has to be parsed back here.
    val d = message.data

    // No activityId means we cannot address a notification. Bail rather than
    // guess: posting under the wrong id creates a duplicate card.
    val activityId = d["activityId"] ?: return
    val event = d["event"] ?: "update"

    // This service is a SEPARATE entry point. It cannot see the module's
    // in-memory state, so every field must come from the payload, and every
    // parse must be defensive, because a crash here kills the update.
    DeliveryNotifier.ensureChannel(this)   // the process may be brand new
    DeliveryNotifier.post(
      ctx = this,
      activityId = activityId,
      // Every `?:` below is load-bearing. toDoubleOrNull returns null rather
      // than throwing on malformed input, so a bad payload degrades the card
      // instead of killing the service.
      orderId = d["orderId"] ?: "",
      status = d["status"] ?: "",
      progress = d["progress"]?.toDoubleOrNull() ?: 0.0,
      etaEpochMillis = d["etaEpochMillis"]?.toDoubleOrNull() ?: 0.0,
      stopsRemaining = d["stopsRemaining"]?.toIntOrNull() ?: 0,
      courierName = d["courierName"] ?: "",
      riderReassigned = d["riderReassigned"]?.toBoolean() ?: false,
      // "end" clears `ongoing`, which lets the user finally swipe the card away.
      ongoing = event != "end",
    )
  }
}
</code></pre>
<p>That "separate entry point" comment is the crux, so it's worth slowing down on. Android can start this service on its own to deliver a push, at a moment when the rest of your app isn't running. There's no React, no module instance, and none of the objects the app was holding in memory. So the push handler can't look up "what delivery is in progress" from app state, because there's no app state to look at. Everything it needs has to come from the push payload itself.</p>
<p>That forces a specific design. The code that builds a notification can't live inside the app and read app state. It has to be standalone code that takes plain values and nothing else.</p>
<p>So I pulled all of it into a shared <code>DeliveryNotifier</code> object, and both paths call it: the in-app path when JavaScript drives an update, and the push path when the service does. Because they run the same builder from the same inputs, they produce an identical notification, and that shared builder is the single most important structural change on the Android side.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c84fbe8c8c80534346db05/652317c7-291c-4480-a402-fe44c60fcb7f.png" alt="DropTrack notification updated by a remote FCM push while the app was backgrounded" style="display:block;margin:0 auto" width="640" height="269" loading="lazy">

<h3 id="heading-the-four-rules-of-android-push">The Four Rules of Android Push</h3>
<p>Four things have to be right, or the push half quietly fails. In short: send the right kind of message, authenticate the harder way, read the error codes correctly, and wire the plugin through prebuild. Each one in full below.</p>
<h4 id="heading-rule-1-send-a-data-only-message-never-one-with-a-notification-block">Rule 1: send a data-only message, never one with a <code>notification</code> block</h4>
<p>This is the rule that catches everyone. A message with a <code>notification</code> block, while the app is backgrounded or killed, is handled by the system tray, and your <code>onMessageReceived</code> code never runs, so the card never updates. Only a data-only message (no <code>notification</code> block) reaches your code in the background.</p>
<p>Firebase documents this in <a href="https://firebase.google.com/docs/cloud-messaging/android/receive">Receive messages in an Android app</a>. Send data-only, and set <a href="https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#androidconfig"><code>android.priority</code></a> to <code>high</code> so the app wakes promptly.</p>
<h4 id="heading-rule-2-authenticate-with-a-service-account-token-not-a-static-key">Rule 2: authenticate with a service-account token, not a static key</h4>
<p>FCM v1 auth is heavier than APNs, with no one-shot <code>.p8</code>. You mint an RS256 JWT from a <a href="https://developers.google.com/identity/protocols/oauth2/service-account">service account</a>, exchange it for a short-lived OAuth access token, and send that access token as a bearer credential to <a href="https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages/send"><code>projects.messages.send</code></a>. The consolation is that FCM v1 is plain HTTPS, so <code>fetch()</code> works.</p>
<pre><code class="language-javascript">// `sa` is the parsed service-account JSON from Firebase. This JWT is not the
// credential you send to FCM; it is the one you trade for a short-lived token.
export function buildAuthJWT(sa) {
  const now = Math.floor(Date.now() / 1000);
  const header = b64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));  // RSA key, so no ieee-p1363 trap
  const claims = b64url(JSON.stringify({
    iss: sa.client_email,
    scope: "https://www.googleapis.com/auth/firebase.messaging",  // exact scope, or every send 403s
    aud: sa.token_uri,        // the JWT is FOR Google's token endpoint
    iat: now,
    exp: now + 3600,          // one hour, Google's maximum
  }));
  const signingInput = `${header}.${claims}`;
  const sig = createSign("RSA-SHA256").update(signingInput).sign(sa.private_key);
  return `${signingInput}.${b64url(sig)}`;
}

export async function sendDataMessage({ token, data, sa }) {
  const accessToken = await getAccessToken(sa);   // trades the JWT for a token; cache the result
  const res = await fetch(
    `https://fcm.googleapis.com/v1/projects/${sa.project_id}/messages:send`,
    {
      method: "POST",
      headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
      body: JSON.stringify({
        message: {
          token,                        // per-DEVICE token (APNs is per-activity)
          data,                         // note: NO `notification` key (see below)
          android: { priority: "high" },
        },
      }),
    }
  );
  const body = await res.json().catch(() =&gt; ({}));   // never throw on a bad error body
  return { status: res.status, reason: body?.error?.status ?? null };
}
</code></pre>
<p>The design turns on two absences and one indirection. The indirection is the token exchange. Unlike APNs, where the JWT <em>is</em> the credential, here you POST the JWT to Google's token endpoint, get back a short-lived access token, and send that, so cache it rather than re-minting per message.</p>
<p>The first absence is any <code>notification</code> key in the body, which is deliberate and covered next. The second is high priority over normal, because <code>normal</code> may be held until the device leaves Doze, by which time the delivery has finished.</p>
<p>On the response, <code>404 UNREGISTERED</code> means drop the token while <code>400 INVALID_ARGUMENT</code> means it was never a token. The caller needs to tell those apart, so the reason string is returned rather than swallowed.</p>
<p>FCM <code>data</code> values are a map of string to string, so everything is stringified on the way out and parsed on the way in. Unlike iOS, there's no silent shape mismatch, because you wrote the parser. But your parser must never throw.</p>
<h4 id="heading-rule-3-treat-400-and-404-as-different-failures">Rule 3: treat <code>400</code> and <code>404</code> as different failures</h4>
<p>FCM splits two cases that APNs collapses. A malformed token returns <code>400 INVALID_ARGUMENT</code>, while a well-formed but unregistered token returns <code>404 UNREGISTERED</code> (the full list is in the <a href="https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode">FCM ErrorCode reference</a>).</p>
<p>APNs reports both as <code>BadDeviceToken</code>. This matters for the fake-token auth probe from the iOS section: on FCM a fake token gives <code>INVALID_ARGUMENT</code>, not <code>UNREGISTERED</code>. Either one still proves your auth works, since a bad service account fails earlier with <code>401</code> or <code>403</code>. I wrote my probe expecting <code>UNREGISTERED</code>, and correcting the test is how I learned the distinction.</p>
<h4 id="heading-rule-4-wire-the-google-services-plugin-through-prebuild">Rule 4: wire the <code>google-services</code> plugin through prebuild</h4>
<p>The <a href="https://developers.google.com/android/guides/google-services-plugin"><code>google-services</code> plugin</a> is what reads your Firebase config into the build. Because Continuous Native Generation regenerates the <code>android</code> folder on every <code>prebuild</code>, a <a href="https://docs.expo.dev/config-plugins/introduction/">config plugin</a> has to re-place <code>google-services.json</code> and re-add the Gradle wiring each time, or the setting is lost on the next prebuild.</p>
<pre><code class="language-javascript">// plugins/withAndroidFcm.js
function withGoogleServicesJson(config) {
  // withDangerousMod runs arbitrary filesystem work during prebuild. It is
  // "dangerous" because nothing validates the result. It is also the only way
  // to place a file the Gradle plugin expects to already exist.
  return withDangerousMod(config, ["android", (cfg) =&gt; {
    // Kept at the repo root, gitignored, and copied in on every prebuild.
    const src = path.join(cfg.modRequest.projectRoot, "google-services.json");
    // Must land in android/app/, where the google-services plugin looks for it.
    const dest = path.join(cfg.modRequest.platformProjectRoot, "app", "google-services.json");

    // Fail loudly at prebuild. Without this the build succeeds, the app starts,
    // and FCM initialisation quietly no-ops at runtime.
    if (!fs.existsSync(src)) {
      throw new Error("[withAndroidFcm] google-services.json not found at project root");
    }

    fs.copyFileSync(src, dest);
    return cfg;
  }]);
}
// plus withProjectBuildGradle for the classpath,
// and withAppBuildGradle to apply the plugin.
</code></pre>
<p>Have it throw when the file is missing. Failing fast at prebuild beats a mystery at runtime.</p>
<h3 id="heading-the-honest-caveat">The Honest Caveat</h3>
<p>A data message can be dropped under <a href="https://developer.android.com/training/monitoring-device-state/doze-standby">Doze</a> or after a force-kill. An iOS Live Activity is system-owned and updates regardless. High priority helps. This is a platform limitation to document rather than engineer around. Test with the app backgrounded, not swiped away, and be honest with your product team about the difference.</p>
<h2 id="heading-three-ux-gaps-the-naive-implementation-leaves">Three UX Gaps the Naïve Implementation Leaves</h2>
<p>Because your code owns the Android side, you inherit three responsibilities that iOS handles quietly for you. I shipped all three only after hitting each one in testing.</p>
<h3 id="heading-tapping-the-notification-does-nothing">Tapping the Notification Does Nothing</h3>
<p><code>NotificationCompat</code> posts happily, but with no <code>setContentIntent(PendingIntent)</code> there is no tap target, so Android just expands and collapses the notification. Nothing in the documentation shouts this at you.</p>
<pre><code class="language-kotlin">private fun launchIntent(ctx: Context, activityId: String): PendingIntent? {
  // Ask the package manager for our own launcher intent, rather than naming
  // MainActivity. Under Expo's Continuous Native Generation that class name is
  // generated, so hardcoding it breaks on the next prebuild.
  val intent = ctx.packageManager.getLaunchIntentForPackage(ctx.packageName)
    // SINGLE_TOP: reuse the existing task instead of stacking a second copy.
    // CLEAR_TOP: drop anything above it, so the user lands on the tracking screen.
    ?.apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP }
    ?: return null

  return PendingIntent.getActivity(
    ctx,
    // The request code. Keyed per activity so two concurrent deliveries get
    // two distinct PendingIntents rather than silently sharing one.
    notificationIdFor(activityId),
    intent,
    // FLAG_IMMUTABLE is mandatory on Android 12+; omit it and this throws.
    // FLAG_UPDATE_CURRENT refreshes the extras of the existing PendingIntent
    // rather than handing back a stale one from an earlier delivery.
    PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
  )
}
</code></pre>
<p><a href="https://developer.android.com/reference/android/app/PendingIntent#FLAG_IMMUTABLE"><code>FLAG_IMMUTABLE</code></a> is required on Android 12 and later. Verify the result with <code>adb shell dumpsys notification</code>, which should show <code>contentIntent=PendingIntent{... startActivity}</code>.</p>
<h3 id="heading-a-push-updates-the-notification-but-not-your-interface">A Push Updates the Notification But Not Your Interface</h3>
<p>The service is a separate entry point from your JavaScript state, so a push arriving while the app is open leaves the app showing a stale step. The fix is a static bridge, set when a module instance exists and cleared when it does not.</p>
<pre><code class="language-kotlin">companion object {
  // Static, because the FCM service cannot reach a module INSTANCE. @Volatile
  // because the service runs on a different thread than the one that sets this.
  // Nullable because, most of the time, there is no live module to emit into.
  @Volatile private var pushEmitter: ((Map&lt;String, String&gt;) -&gt; Unit)? = null

  // Safe to call from anywhere. The `?.` is the entire "is the app alive?" check.
  fun emitPush(data: Map&lt;String, String&gt;) { pushEmitter?.invoke(data) }
}

override fun definition() = ModuleDefinition {
  // Declares the events JS may subscribe to. Emitting an undeclared name throws.
  Events("onFcmTokenReceived", "onDeliveryPush")

  // Register the bridge when a module instance exists...
  OnCreate  { pushEmitter = { data -&gt; sendEvent("onDeliveryPush", data) } }
  // ...and tear it down when it does not. Skip this and you leak a closure
  // holding a dead React context, then crash on the next push.
  OnDestroy { pushEmitter = null }
  // ...
}
</code></pre>
<p>The service calls <code>DroptrackLiveModule.emitPush(d)</code> after <code>notify()</code>. It does nothing when the app is dead, in which case the notification still updates and there's simply nothing to sync. This is the Android analogue of the iOS foreground resync.</p>
<h3 id="heading-a-cold-started-app-comes-up-empty">A Cold-started App Comes Up Empty</h3>
<p>iOS recovers from <code>Activity.activities</code>. Android has no such store, so a killed app reopened by tapping its own notification shows "Not tracking". Persist the delivery to <a href="https://developer.android.com/reference/android/content/SharedPreferences"><code>SharedPreferences</code></a> on every <code>notify()</code>, then read it back.</p>
<pre><code class="language-kotlin">// Same JS name as the iOS implementation, so App.tsx never branches on platform.
AsyncFunction("getRunningActivities") {
  val ctx = context ?: return@AsyncFunction emptyList&lt;Map&lt;String, Any?&gt;&gt;()

  // Our stand-in for iOS's Activity.activities: the record written on notify().
  val active = DeliveryNotifier.activeDelivery(ctx)
    ?: return@AsyncFunction emptyList&lt;Map&lt;String, Any?&gt;&gt;()
  val activityId = active["activityId"] as String

  // Phantom guard: trust the system's notification list over our own record.
  if (!DeliveryNotifier.isActive(ctx, activityId)) {
    DeliveryNotifier.clear(ctx)
    return@AsyncFunction emptyList&lt;Map&lt;String, Any?&gt;&gt;()
  }

  // Repopulate the per-process map, or a later update/end throws after cold start.
  deliveries[activityId] = DeliveryInfoRecord().apply { orderId = active["orderId"] as String }
  return@AsyncFunction listOf(active)
}
</code></pre>
<p>Two subtleties here I only found by breaking them. Guard the rehydration with <a href="https://developer.android.com/reference/android/app/NotificationManager#getActiveNotifications()"><code>getActiveNotifications()</code></a>. Without that check, a stale record produces an uncancellable phantom delivery whose Cancel button throws <code>ActivityNotFoundException</code>. And make <code>endDelivery</code> forgiving. If the activity isn't in the in-memory map because you cold-started, still cancel the notification and clear the state rather than throwing. Tearing something down should never fail because you've forgotten about it.</p>
<p>Because the resync effect in <code>App.tsx</code> is cross-platform, implementing Android's <code>getRunningActivities()</code> lights up the existing cold-start path with no JavaScript changes. That's the payoff of keeping one API across two backends.</p>
<h2 id="heading-how-to-script-the-simulators-and-devices">How to Script the Simulators and Devices</h2>
<p>Two small tools saved more time than any feature.</p>
<p>The iOS simulator has no scriptable tap. There's no <code>uiautomator</code> equivalent, and synthetic clicks need macOS accessibility grants. So I added a development-only deep-link driver, fifteen lines, stripped from release builds.</p>
<pre><code class="language-typescript">useEffect(() =&gt; {
  // Dead code in release builds: the bundler strips the whole effect body.
  if (!__DEV__) return;

  // "droptrack://drive/next" -&gt; "next" -&gt; call actions.next().
  // actionsRef, not actions: the listener is registered once, so a plain
  // closure would capture the first render's handlers forever.
  const run = (url: string) =&gt; actionsRef.current[url.split("/").pop() ?? ""]?.();

  // Case 1: app already running. simctl openurl fires this event.
  const sub = Linking.addEventListener("url", ({ url }) =&gt; run(url));

  // Case 2: devicectl --payload-url COLD-STARTS the app: the URL arrives as the
  // initial URL and never fires the 'url' event. Handle both or device
  // automation silently does nothing while simulator automation works.
  void Linking.getInitialURL().then((url) =&gt; url &amp;&amp; run(url));

  return () =&gt; sub.remove();
}, []);
</code></pre>
<pre><code class="language-shell"># Drives the running app one step forward, with no tap and no accessibility grant.
xcrun simctl openurl booted "com.fasarticle.droptrack://drive/next"
</code></pre>
<p>For the push half I built a small web dispatcher. A browser cannot reach APNs or FCM, because of HTTP/2, missing cross-origin headers, and the fact that signing keys must never leave the server. So it talks to a zero-dependency local Node.js signing server that scrapes push tokens off the device console, streams them to the browser with server-sent events, and sends to whichever platform you select.</p>
<pre><code class="language-text">iPhone  --NSLog--&gt; devicectl --console --+
Android --Log.i--&gt; adb logcat -----------+
                                         | scrape
browser &lt;--SSE /events-- dispatch-server (127.0.0.1:8787)
   \--POST /push--------&gt; apns.mjs / fcm.mjs --&gt; APNs / FCM --&gt; device
</code></pre>
<p>Pick a step, pick a courier, and press one button. It turns a two-minute test into a ten-second one, and it made the Samsung investigation below possible at all.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c84fbe8c8c80534346db05/ac41eefe-7672-4668-a26d-2e375879621b.png" alt="The DropTrack web dispatcher, one control plane for iOS and Android pushes" style="display:block;margin:0 auto" width="900" height="1546" loading="lazy">

<h2 id="heading-how-ios-and-android-compare">How iOS and Android Compare</h2>
<table>
<thead>
<tr>
<th></th>
<th>iOS Live Activity</th>
<th>Android Live Update</th>
</tr>
</thead>
<tbody><tr>
<td>Introduced</td>
<td>iOS 16.1 and 16.2</td>
<td>Android 16 (API 36), promotion in 36.1</td>
</tr>
<tr>
<td>Interface</td>
<td>Custom SwiftUI in a widget extension</td>
<td>System <code>ProgressStyle</code> template, custom <code>RemoteViews</code> not allowed when promoted</td>
</tr>
<tr>
<td>Surfaces</td>
<td>Lock screen and Dynamic Island, plus Mac via Continuity</td>
<td>Shade, status-bar chip, promoted lock-screen slot</td>
</tr>
<tr>
<td>Update while app closed</td>
<td>Per-activity APNs token. The system updates the widget</td>
<td>Re-post the same notification id. Remote means an FCM data message that wakes your service</td>
</tr>
<tr>
<td>Progress bar</td>
<td>Hand-built, an <code>HStack</code> of capsules</td>
<td><code>ProgressStyle</code> segments and points</td>
</tr>
<tr>
<td>Promotion rules</td>
<td>Automatic once started</td>
<td>Permission, request, ongoing, title, allowed style, importance above <code>MIN</code>, not colorized</td>
</tr>
<tr>
<td>Push auth</td>
<td>ES256 <code>.p8</code> key, HTTP/2</td>
<td>Service-account RS256 to OAuth token, plain HTTPS</td>
</tr>
<tr>
<td>Payload decoding</td>
<td>The system decodes into your <code>Codable</code>. A mismatch drops silently</td>
<td>You parse it yourself in your service</td>
</tr>
<tr>
<td>Cold-start recovery</td>
<td><code>Activity.activities</code>, owned by the system</td>
<td>You persist and read it back yourself</td>
</tr>
<tr>
<td>Tap to open</td>
<td>Free</td>
<td>You must attach a content <code>PendingIntent</code></td>
</tr>
<tr>
<td>Delivery guarantee</td>
<td>System-owned and reliable</td>
<td>Data message, droppable under Doze or force-kill</td>
</tr>
</tbody></table>
<h2 id="heading-the-samsung-reality-check">The Samsung Reality Check</h2>
<p>Version numbers lie, and Samsung is where they lie loudest. I tested by hand across three real Galaxy devices using <a href="https://developer.samsung.com/remote-test-lab">Samsung Remote Test Lab</a>, whose Remote Debug Bridge turns out to be a full local <code>adb</code> tunnel to a phone in Korea. The entire emulator automation playbook (granting permissions, tapping, taking screenshots, and reading <code>dumpsys</code>) works unchanged against remote hardware.</p>
<p>Here's what the same unmodified APK did on each:</p>
<table>
<thead>
<tr>
<th>Device</th>
<th>One UI / SDK</th>
<th><code>canPostPromotedNotifications()</code></th>
<th>What actually showed</th>
</tr>
</thead>
<tbody><tr>
<td>Galaxy S25 Ultra</td>
<td>8.0 / 36.0</td>
<td><code>false</code></td>
<td>Nothing. No chip, no promoted card</td>
</tr>
<tr>
<td>Galaxy S26 Ultra</td>
<td>8.5 / 36.1</td>
<td><code>true</code></td>
<td>Top-of-shade pinning and a status-bar icon</td>
</tr>
<tr>
<td>Galaxy A37 (mid-range)</td>
<td>8.5 / 36.1</td>
<td><code>true</code></td>
<td>Same as the S26</td>
</tr>
</tbody></table>
<p>Two findings come out of that table.</p>
<h4 id="heading-1-on-the-base-36-device-the-two-capability-checks-disagree">1. On the base-36 device, the two capability checks disagree</h4>
<p>The S25 Ultra runs base 36, which has no promotion pipeline, so nothing promotes. But <code>hasPromotableCharacteristics()</code> returns <code>true</code> there (Samsung backported some framework pieces) while <code>canPostPromotedNotifications()</code> returns <code>false</code>. A base-36 Pixel emulator returns <code>false</code> for both. So you can't infer one check from the other. Detect both at runtime, and trust <code>canPostPromotedNotifications()</code> for whether promotion will actually happen.</p>
<h4 id="heading-2-on-the-361-devices-promotion-works-but-only-partly">2. On the 36.1 devices, promotion works but only partly</h4>
<p>Both the S26 Ultra and the A37 genuinely grant <code>FLAG_PROMOTED_ONGOING</code> to the unmodified APK, which you can confirm in <code>dumpsys</code>. You get top-of-shade pinning and the status-bar icon. You do <em>not</em> get the chip pill, the lock-screen card, or a Now Bar entry, even though the promotion succeeded.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c84fbe8c8c80534346db05/71557971-c9d3-4936-ab7d-1ee9b2b779ef.png" alt="DropTrack promoted to the top of the shade on Samsung One UI 8.5" style="display:block;margin:0 auto" width="640" height="1386" loading="lazy">

<p>That "only partly" was the mystery, until I found where the missing surfaces went. In Settings, under Lock screen and Always On Display, there's a page called Live notifications (searchable as "live notification", notably not as "Now bar"). It promises exactly the surfaces I was missing: the lock screen, the status bar, and the top of the notification panel, illustrated with the Now Bar pill. And directly below that promise sits a fixed, six-app allowlist: Audio broadcast, Emergency sharing, Google Finance, Maps, Media player, and Sports from Google.</p>
<p>DropTrack isn't on that list. Yet its promoted delivery was live on that very device at that very second, and the page's own "Not seeing Live notifications?" checklist (three notification permissions) was fully satisfied. The feature was working. Samsung simply doesn't offer its best surfaces to apps outside the six.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c84fbe8c8c80534346db05/5b2ad0b1-7c3d-4b96-ab37-8d82c72b09fb.png" alt="Samsung's Live notifications settings page showing a fixed six-app allowlist" style="display:block;margin:0 auto" width="640" height="1386" loading="lazy">

<p>So the screenshot-backed conclusion for these units is this: One UI 8.5 accepts Google's promotion contract and ships the minor surfaces to any app, but reserves the headline ones for a hardcoded list. This reflects those specific devices at the time of testing, and Samsung may change it.</p>
<p>It leaves the three platforms in three different places. Apple offers a public API on every device. Google offers a public API on its own hardware. Samsung offers the framework to everyone but keeps the best stage by invitation.</p>
<h2 id="heading-the-demo-repository">The Demo Repository</h2>
<p>Everything in this handbook is one working project, <a href="https://github.com/FastheDeveloper/LiveActivity">DropTrack</a>, released under the MIT licence. It's not a snippet dump. It's a single React Native app whose one delivery-tracking feature reaches down through five layers. The point of reading it is to see how those layers connect rather than how any one of them looks in isolation.</p>
<p>Here's what lives in the repo, layer by layer:</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Where</th>
<th>What is there</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Mobile app</strong></td>
<td><code>App.tsx</code>, <code>delivery.ts</code></td>
<td>The React Native UI and the one cross-platform TypeScript API (<code>startDelivery</code> / <code>updateDelivery</code> / <code>endDelivery</code>) that both native sides implement</td>
</tr>
<tr>
<td><strong>iOS native</strong></td>
<td><code>modules/droptrack-live/ios/</code>, <code>targets/widgets/</code></td>
<td>The Swift ActivityKit bridge and the SwiftUI widget with its four presentations, the segmented bar, and the reassignment treatment</td>
</tr>
<tr>
<td><strong>Android native</strong></td>
<td><code>modules/droptrack-live/android/</code></td>
<td>The Kotlin <code>NotificationCompat</code> builder, the promotion logic, and the <code>FirebaseMessagingService</code> that re-posts from a push</td>
</tr>
<tr>
<td><strong>Web</strong></td>
<td><code>DispatcherConsole.tsx</code>, <code>src/dispatchClient.ts</code></td>
<td>The Expo-web dispatcher console, a browser UI for composing and firing pushes at a chosen device</td>
</tr>
<tr>
<td><strong>Backend</strong></td>
<td><code>scripts/</code></td>
<td>The APNs client (about 100 lines) and the FCM client (about 80 lines) written from scratch with no push libraries, plus the local signing server that ties the console to real devices</td>
</tr>
</tbody></table>
<p>A few things make it worth cloning rather than skimming:</p>
<ul>
<li><p><strong>It genuinely runs on all three surfaces:</strong> The same <code>DeliveryState</code> object drives a SwiftUI Live Activity, an Android promoted notification, and a web console, so you can watch one API produce three very different results.</p>
</li>
<li><p><strong>The push clients have no dependencies:</strong> <code>scripts/apns.mjs</code> and <code>scripts/fcm.mjs</code> use only Node built-ins, so you can read the entire APNs and FCM path end to end without unpacking a library. Both have small test files next to them.</p>
</li>
<li><p><strong>Every gotcha in this article is written up in</strong> <a href="https://github.com/FastheDeveloper/LiveActivity/blob/main/GOTCHAS.md"><code>GOTCHAS.md</code></a><strong>:</strong> The long checklist that used to live in this section now lives there, next to the code it refers to, alongside <code>DEVLOG.md</code> (how the build unfolded) and <code>ARTICLE_NOTES.md</code>.</p>
</li>
<li><p><strong>It's safe to fork:</strong> The Firebase service account, the <code>google-services.json</code>, and the APNs <code>.p8</code> key are all gitignored, so nothing sensitive is in the history. The <code>README.md</code> lists exactly which of those you supply to run the push phase yourself.</p>
</li>
</ul>
<p>Clone it, run the app on a simulator, open the web console, and push an update to your own device. That loop is the fastest way to make everything above concrete.</p>
<h2 id="heading-what-to-know-before-you-start">What to Know Before You Start</h2>
<p><strong>The product feature is identical, and the platform contracts are opposites.</strong> iOS gives you a system-owned widget and updates it for you. Android gives you a notification and makes you the update engine, down to re-posting it from a background service on every message. Design your module's seam accordingly. The same three-function API can hide wildly different machinery, and that is exactly what a good native module is for.</p>
<p><strong>The silent failures are the tax.</strong> Almost every hard bug in this project, from the mismatched widget struct, to the missing entitlement, the wrong epoch, the un-promoted notification, and the <code>notification</code>-instead-of-<code>data</code> message, fails with zero errors. So instrument aggressively. Log promotable characteristics. Verify entitlements survived signing. Probe push auth with fake tokens before you involve a device. Always confirm on the real surface rather than trusting a <code>200</code>.</p>
<p><strong>Version numbers lie, so detect features instead.</strong> Android 16 is two releases. Samsung's Android 16 is a third. <code>canPostPromotedNotifications()</code> and <code>hasPromotableCharacteristics()</code> can disagree on the same build. Check both at runtime, and never infer one from the other.</p>
<p><strong>The system owns the activity, not your process.</strong> On iOS the activity outlives your variable, your React state, and your process. On Android the notification outlives your process too, but nothing recovers it for you. Both platforms punish you for assuming the id in your <code>useState</code> is the source of truth.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You now have a complete picture of both platforms. You built one TypeScript API over two native backends, a SwiftUI widget with four presentations, a Kotlin notification that satisfies Android's promotion contract, and two push clients written from scratch with no libraries. You also know the failure modes that produce no error message, which is most of them.</p>
<p>Three things are worth exploring next:</p>
<ol>
<li><p><strong>Push-to-start, on iOS 17.2 and later.</strong> You can start a Live Activity from a push, with no app launch at all. The token is per-app rather than per-activity, which rewrites the token plumbing described above.</p>
</li>
<li><p><strong>Broadcast channels, on iOS 18.</strong> One push updates an activity for many users, which suits live scores. Every push in this handbook targets a single token.</p>
</li>
<li><p><strong>An Android foreground service.</strong> It narrows the Doze and force-kill delivery gap. It won't close it, but it makes long-running deliveries more durable than a bare data message.</p>
</li>
</ol>
<p>The interesting part was never <code>Activity.request()</code>. It was everything the two platforms decline to tell you when you get it wrong.</p>
<h2 id="heading-sources-and-further-reading">Sources and Further Reading</h2>
<p>Apple, ActivityKit and APNs:</p>
<ul>
<li><p><a href="https://developer.apple.com/documentation/activitykit">ActivityKit</a> and <a href="https://developer.apple.com/documentation/activitykit/activityattributes"><code>ActivityAttributes</code></a></p>
</li>
<li><p><a href="https://developer.apple.com/documentation/activitykit/displaying-live-data-with-live-activities">Displaying live data with Live Activities</a></p>
</li>
<li><p><a href="https://developer.apple.com/documentation/activitykit/starting-and-updating-live-activities-with-activitykit-push-notifications">Starting and updating Live Activities with ActivityKit push notifications</a></p>
</li>
<li><p><a href="https://developer.apple.com/design/human-interface-guidelines/live-activities">Live Activities, Human Interface Guidelines</a></p>
</li>
<li><p><a href="https://developer.apple.com/documentation/bundleresources/entitlements/aps-environment">The <code>aps-environment</code> entitlement</a></p>
</li>
<li><p><a href="https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns">Sending notification requests to APNs</a>, <a href="https://developer.apple.com/documentation/usernotifications/establishing-a-token-based-connection-to-apns">Establishing a token-based connection to APNs</a>, and <a href="https://developer.apple.com/documentation/usernotifications/handling-notification-responses-from-apns">Handling notification responses from APNs</a></p>
</li>
<li><p><a href="https://developer.apple.com/documentation/foundation/date"><code>Date</code>, and the 2001 reference date</a></p>
</li>
<li><p><a href="https://developer.apple.com/documentation/widgetkit">WidgetKit</a></p>
</li>
</ul>
<p>Android and Firebase:</p>
<ul>
<li><p><a href="https://developer.android.com/about/versions/16/features/progress-centric-notifications">Progress-centric notifications in Android 16</a> and <a href="https://developer.android.com/about/versions/16">Android 16 overview</a></p>
</li>
<li><p><a href="https://developer.android.com/reference/android/app/Notification.ProgressStyle"><code>Notification.ProgressStyle</code></a> and <a href="https://developer.android.com/reference/androidx/core/app/NotificationCompat.ProgressStyle"><code>NotificationCompat.ProgressStyle</code></a></p>
</li>
<li><p><a href="https://developer.android.com/reference/androidx/core/app/NotificationCompat.Builder"><code>NotificationCompat.Builder</code></a>, where <code>setRequestPromotedOngoing</code> and <code>setShortCriticalText</code> are backported</p>
</li>
<li><p><a href="https://developer.android.com/reference/android/os/Build.VERSION#SDK_INT_FULL"><code>Build.VERSION.SDK_INT_FULL</code></a> and <a href="https://developer.android.com/reference/android/os/Build.VERSION_CODES_FULL"><code>Build.VERSION_CODES_FULL</code></a>, the 36 versus 36.1 distinction</p>
</li>
<li><p><a href="https://developer.android.com/reference/android/app/NotificationManager#canPostPromotedNotifications()"><code>NotificationManager.canPostPromotedNotifications()</code></a>, <a href="https://developer.android.com/reference/android/app/Notification#hasPromotableCharacteristics()"><code>Notification.hasPromotableCharacteristics()</code></a>, and <a href="https://developer.android.com/reference/android/app/NotificationManager#getActiveNotifications()"><code>getActiveNotifications()</code></a></p>
</li>
<li><p><a href="https://developer.android.com/reference/android/app/PendingIntent#FLAG_IMMUTABLE"><code>PendingIntent.FLAG_IMMUTABLE</code></a> and <a href="https://developer.android.com/reference/android/content/SharedPreferences"><code>SharedPreferences</code></a></p>
</li>
<li><p><a href="https://developer.android.com/jetpack/androidx/releases/core"><code>androidx.core</code> release notes</a></p>
</li>
<li><p><a href="https://developer.android.com/training/monitoring-device-state/doze-standby">Doze and App Standby</a></p>
</li>
<li><p><a href="https://firebase.google.com/docs/cloud-messaging">Firebase Cloud Messaging</a>, <a href="https://firebase.google.com/docs/cloud-messaging/android/receive">Receive messages in an Android app</a>, <a href="https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/FirebaseMessagingService"><code>FirebaseMessagingService</code></a>, <a href="https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages/send"><code>projects.messages.send</code></a>, and the <a href="https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode">FCM <code>ErrorCode</code> reference</a></p>
</li>
<li><p><a href="https://developers.google.com/identity/protocols/oauth2/service-account">Using OAuth 2.0 for server to server applications</a> and the <a href="https://developers.google.com/android/guides/google-services-plugin"><code>google-services</code> Gradle plugin</a></p>
</li>
</ul>
<p>Apps that document their own Live Activity, cited in "Where You Have Already Seen This Feature":</p>
<ul>
<li><p><a href="https://apps.apple.com/us/app/chowdeck-food-delivery/id1530676376">Chowdeck</a>, <a href="https://apps.apple.com/us/app/espn-live-sports-scores/id317469184">ESPN</a>, <a href="https://apps.apple.com/us/app/mlb/id493619333">MLB</a>, <a href="https://apps.apple.com/us/app/fotmob-soccer-live-scores/id488575683">FotMob</a>, <a href="https://apps.apple.com/us/app/carrot-weather-alerts-radar/id961390574">CARROT Weather</a>, and <a href="https://apps.apple.com/us/app/structured-daily-planner-todo/id1499198946">Structured</a>, all via their App Store listings</p>
</li>
<li><p><a href="https://flighty.com/help/live-activities-widgets">Flighty's Live Activities help page</a></p>
</li>
<li><p><a href="https://support.apple.com/guide/apple-sports-app/follow-games-in-real-time-apdc0cb7ad64/web">Apple Sports, following games in real time</a></p>
</li>
<li><p>MacRumors on <a href="https://www.macrumors.com/2023/05/02/uber-eats-live-activities/">Uber Eats, May 2023</a> and <a href="https://www.macrumors.com/2023/12/04/doordash-rolling-out-live-activities/">DoorDash, December 2023</a></p>
</li>
<li><p>MacStories, <a href="https://www.macstories.net/reviews/ios-16-1-and-apps-with-live-activities-the-macstories-roundup-part-1/">the iOS 16.1 Live Activities roundup</a></p>
</li>
</ul>
<p>React Native, Expo, and tooling:</p>
<ul>
<li><p><a href="https://docs.expo.dev/modules/overview/">Expo Modules API overview</a> and the <a href="https://docs.expo.dev/modules/module-api/">module API reference</a></p>
</li>
<li><p><a href="https://docs.expo.dev/config-plugins/introduction/">Expo config plugins</a> and <a href="https://docs.expo.dev/workflow/continuous-native-generation/">Continuous Native Generation</a></p>
</li>
<li><p><a href="https://github.com/EvanBacon/expo-apple-targets"><code>@bacons/apple-targets</code></a>, which generates the widget extension</p>
</li>
<li><p><a href="https://github.com/software-mansion-labs/expo-live-activity"><code>expo-live-activity</code></a>, the packaged alternative</p>
</li>
<li><p><a href="https://github.com/invertase/notifee">Notifee</a>, archived, and its last release <a href="https://www.npmjs.com/package/@notifee/react-native"><code>@notifee/react-native@9.1.8</code></a></p>
</li>
<li><p>React Native <a href="https://reactnative.dev/docs/appstate"><code>AppState</code></a> and <a href="https://reactnative.dev/docs/linking"><code>Linking</code></a></p>
</li>
<li><p>Node.js <a href="https://nodejs.org/api/crypto.html"><code>crypto</code></a> and <a href="https://nodejs.org/api/http2.html"><code>http2</code></a></p>
</li>
<li><p><a href="https://datatracker.ietf.org/doc/html/rfc7519">RFC 7519, JSON Web Token</a> and <a href="https://datatracker.ietf.org/doc/html/rfc7518">RFC 7518, JSON Web Algorithms</a>, which define the ES256 and RS256 signature formats</p>
</li>
<li><p><a href="https://developer.samsung.com/remote-test-lab">Samsung Remote Test Lab</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Run Private Text-to-Speech on Your Own Hardware Using QVAC ]]>
                </title>
                <description>
                    <![CDATA[ When I was putting the final touches on QuizRope, an educational mobile app I built that uses LLMs for real-time tutoring and homework assistance, I knew the next logical step was voice. Reading text  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-run-private-text-to-speech-on-your-own-hardware-using-qvac/</link>
                <guid isPermaLink="false">6a2e0cb22e4a72670f854140</guid>
                
                    <category>
                        <![CDATA[ Machine Learning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TextToSpeech ]]>
                    </category>
                
                    <category>
                        <![CDATA[ privacy ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jibril-M🍀 ]]>
                </dc:creator>
                <pubDate>Sun, 14 Jun 2026 02:06:42 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/3ac11484-05eb-4e59-9d35-f2bad4d1d730.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When I was putting the final touches on <a href="https://github.com/DjibrilM/Quiz-rope-">QuizRope</a>, an educational mobile app I built that uses LLMs for real-time tutoring and homework assistance, I knew the next logical step was voice. Reading text on a screen is great, but having an AI tutor physically <em>speak</em> to you transforms the entire learning experience.</p>
<p>Naturally, my first instinct was to look at cloud providers. While services like ElevenLabs offer incredible voice quality, I quickly ran the numbers. Between the API pricing, token consumption for lengthy tutoring sessions, and the sheer volume of users I anticipated, the math got ugly very quickly. Relying on a paid API for every single sentence spoken within the app simply wasn't sustainable for an independent developer.</p>
<p>If you’re about to ask, "How far did you get with QuizRope?", well honestly, I straight-up gave up on the project back then because I couldn't find a sane, affordable solution for the TTS feature.</p>
<p>Beyond the prohibitive cost, there was the latency. Waiting for a server to process a prompt, generate the audio, and stream it back down to a mobile device completely breaks the conversational illusion. And worst of all, it meant every question a student asked would be beamed to a third-party server.</p>
<p>That frustration became the catalyst for my search to find a reliable, offline, and completely zero-cost solution.</p>
<p>In this article, we’re going to build a React Native application that performs high-fidelity Text-to-Speech (TTS) completely offline using your device's own hardware.</p>
<p>If you haven't set up your environment or need a refresher on local inference fundamentals, I highly recommend reading my previous article, <a href="https://www.freecodecamp.org/news/how-to-run-an-llm-locally-on-your-mobile-phone-with-qvac-and-expo/">How to Run a Local LLM Offline in React Native with QVAC</a>, where I cover project initialization, prebuilding, and native hardware dependencies.</p>
<p>This guide assumes you already have a project with the QVAC SDK configured and ready to run on a physical device.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-is-qvac">What is QVAC?</a></p>
</li>
<li><p><a href="#heading-the-architecture-supported-by-qvac">The Architecture Supported by QVAC</a></p>
</li>
<li><p><a href="#heading-the-inference-pipeline">The Inference Pipeline</a></p>
</li>
<li><p><a href="#heading-environment-and-dependency-config">Environment and Dependency Config</a></p>
</li>
<li><p><a href="#heading-the-audio-utility-packaging">The Audio Utility Packaging</a></p>
</li>
<li><p><a href="#heading-complete-implementation">Complete Implementation</a></p>
</li>
<li><p><a href="#heading-codebase-breakdown">Codebase Breakdown</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-resources-and-further-reading">Resources and Further Reading</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To get the most out of this article, you should have a solid foundation in modern web and mobile development:</p>
<ul>
<li><p><strong>JavaScript/TypeScript &amp; React</strong>: Familiarity with React concepts and hooks, especially <code>useState</code>, <code>useEffect</code>, and <code>useRef</code>.</p>
</li>
<li><p><strong>React Native &amp; Expo</strong>: Basic understanding of layout structures (such as <code>View</code>, <code>ScrollView</code>, <code>TextInput</code>) and styling conventions.</p>
</li>
<li><p><strong>Asynchronous JavaScript &amp; Binary Buffers</strong>: Experience with <code>async/await</code>, Promises, and basic manipulation of arrays like <code>Int16Array</code> or <code>Buffer</code>.</p>
</li>
<li><p><strong>Development Build Environment</strong>: Familiarity with running local development compilation commands, specifically <code>npx expo prebuild</code> to build native iOS and Android modules.</p>
</li>
<li><p><strong>Physical Mobile Device</strong>: Because local machine learning models leverage device-specific hardware acceleration and native optimizations, the QVAC SDK doesn't support simulator environments. You must have a physical iOS or Android testing device with Developer Mode enabled.</p>
</li>
</ul>
<h2 id="heading-what-is-qvac">What is QVAC?</h2>
<p>To help you follow along more effectively, let’s establish what QVAC is and why it exists.</p>
<p>Developed by Tether, QVAC is a local-first AI SDK designed for building cross-platform, peer-to-peer (P2P) applications and systems.</p>
<p>Many mobile applications that utilize Large Language Models (LLMs) or Text-to-Speech (TTS) engines rely on network requests to cloud-hosted APIs (such as OpenAI or ElevenLabs). While convenient, this model introduces dependencies on network connectivity, recurring API usage fees, and transmission of user data to third-party servers.</p>
<p>QVAC provides an alternative by executing AI models directly on the client device. This local-first architecture offers several practical advantages:</p>
<ul>
<li><p><strong>Local-first execution</strong>: Runs inference directly on the client hardware, eliminating the need for external APIs or active internet connections.</p>
</li>
<li><p><strong>Peer-to-peer (P2P) support</strong>: Allows distributing inference tasks across local networks, helping coordinate workloads without centralized servers.</p>
</li>
<li><p><strong>Cross-platform compatibility</strong>: Provides a single JavaScript/TypeScript interface that works consistently across different hardware and runtime environments.</p>
</li>
<li><p><strong>Unified capabilities</strong>: Exposes text generation, transcription, image generation, and speech synthesis within a single package.</p>
</li>
</ul>
<h3 id="heading-key-concepts-for-on-device-inference">Key Concepts for On-Device Inference</h3>
<p>To understand how QVAC runs on a mobile device, we must keep a few key concepts in mind:</p>
<ul>
<li><p><strong>On-Device Inference</strong>: Running model calculations locally. Rather than relying on a single engine, QVAC supports multiple specialized local inference backends depending on the task (such as <code>llama.cpp</code> for text, <code>whisper.cpp</code> for transcription, or custom diffusion backends for image generation). Under the hood, these engines memory-map quantized model weights directly into the device's RAM and run calculations using native GPU hardware acceleration.</p>
</li>
<li><p><strong>Quantization (GGUF format)</strong>: A mathematical optimization technique that compresses the model's weights (for example, from a standard 16-bit floating-point precision down to 4-bit or 8-bit integers). This makes it possible for models to fit into the memory constraints of consumer mobile hardware while keeping output quality high.</p>
</li>
<li><p><strong>KV (Key-Value) Cache</strong>: A memory area that stores calculated states of previous tokens so the model doesn't have to re-evaluate the entire context window with every word or token it generates.</p>
</li>
</ul>
<h2 id="heading-the-architecture-supported-by-qvac">The Architecture Supported by QVAC</h2>
<p>Before writing code, it's crucial to understand what's actually happening under the hood. To handle local execution without melting your device, the QVAC SDK manages the hardware binding and model lifecycle while hooking into optimized, community-maintained <a href="https://huggingface.co/blog/introduction-to-ggml"><strong>GGML</strong></a> inference backends.</p>
<p>Instead of a one-size-fits-all approach, the QVAC SDK supports two distinctly different neural architectures for speech synthesis. Depending on your application's needs — whether you want instant voice cloning or ultra-high-fidelity pre-trained voices — you'll choose between <strong>Chatterbox</strong> and <strong>Supertonic</strong>.</p>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Chatterbox</th>
<th>Supertonic</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Architecture</strong></td>
<td>Transformer-based language model</td>
<td>Diffusion-based latent denoising</td>
</tr>
<tr>
<td><strong>Model Structure</strong></td>
<td>Split (T3 GGUF + S3Gen companion)</td>
<td>Single file (GGUF)</td>
</tr>
<tr>
<td><strong>Voice Method</strong></td>
<td>Zero-shot voice cloning (Reference WAV)</td>
<td>Pre-trained voice styles</td>
</tr>
<tr>
<td><strong>Sample Rate</strong></td>
<td>24,000 Hz</td>
<td>44,100 Hz</td>
</tr>
</tbody></table>
<h3 id="heading-1-the-chatterbox-engine">1. The Chatterbox Engine</h3>
<p>Chatterbox is built on a <strong>transformer-based language model</strong> architecture. It treats audio generation similarly to how an LLM predicts the next word in a sentence, but instead, it predicts discrete acoustic tokens.</p>
<p>Because of this architecture, Chatterbox excels at <strong>zero-shot voice cloning</strong>. Instead of relying purely on pre-baked voices, you can pass an optional <code>referenceAudioSrc</code> (a short WAV file of someone speaking) alongside your text. The transformer analyzes the reference audio's acoustic properties and generates a cloned voice based on those features.</p>
<h3 id="heading-2-the-supertonic-engine">2. The Supertonic Engine</h3>
<p>Supertonic takes a completely different approach, utilizing <a href="https://www.emergentmind.com/topics/latent-denoising-diffusion-models"><strong>diffusion-based latent denoising</strong></a> — the same fundamental architecture used by AI image generators like Stable Diffusion, but applied to audio.</p>
<p>It starts with pure digital noise and iteratively refines it into a 44.1 kHz high-fidelity speech waveform based on the text prompt. Supertonic uses a single, unified GGUF file rather than a split model. Instead of dynamic voice cloning, it relies on highly optimized, pre-trained voice styles (for example, <code>voice: "F1"</code> or <code>voice: "M1"</code>) baked directly into the model. This makes it incredibly efficient for generating crystal-clear, studio-quality speech when you don't need dynamic cloning capabilities.</p>
<p>For this tutorial, we'll use Supertonic. It yields fantastic results out of the box and avoids the complexity of loading multiple companion files.</p>
<h2 id="heading-the-inference-pipeline">The Inference Pipeline</h2>
<p>To visualize how we interact with these engines in our codebase, think of local TTS (Text to Speech) as running a virtual recording studio right in your phone's memory:</p>
<ol>
<li><p><strong>Hiring the actor (loading the model):</strong> We map the compressed GGUF file directly into the device's RAM or GPU VRAM.</p>
</li>
<li><p><strong>Handing over the script (text input):</strong> We pass plain text to the loaded engine.</p>
</li>
<li><p><strong>The performance (inference):</strong> The engine reads the text and mathematically predicts the sound waves. Crucially, the AI doesn't emit a finished audio file. Instead, it outputs raw digital sound waves known as PCM samples.</p>
</li>
<li><p><strong>Packaging the audio:</strong> Because a raw list of numbers can't be played by standard media players, we must manually wrap the PCM data in a standard WAV header.</p>
</li>
<li><p><strong>Closing the studio (unloading):</strong> Because speech synthesis is memory-intensive and maintains a persistent state, the model is cleared from RAM to free up resources and flush its context.</p>
</li>
</ol>
<h2 id="heading-environment-and-dependency-config">Environment and Dependency Config</h2>
<p>Before we jump into the codebase, there's a crucial dependency setup to keep in mind if your project uses the pnpm package manager.</p>
<p>Because QVAC plugins rely on transitive native peer dependencies, strict package managers like pnpm will lock these dependencies down inside hidden <code>.pnpm</code> subfolders.</p>
<p>To ensure the QVAC native bundler (<code>bare-pack</code>) can resolve your worker plugins correctly at build time, create a <code>.npmrc</code> file in the root of your project:</p>
<pre><code class="language-ini">shamefully-hoist=true
</code></pre>
<p>IMPORTANT: After creating this file, you must run a clean dependency install (<code>pnpm install</code>). This ensures a flat layout in your root <code>node_modules</code> so that all QVAC-specific helper packages are resolved properly during your local <code>npx expo prebuild</code> compilation step.</p>
<h2 id="heading-the-audio-utility-packaging">The Audio Utility Packaging</h2>
<p>Because QVAC outputs raw PCM arrays, we need to construct a valid WAV file in memory and write it to the device's storage before the native audio player can play it.</p>
<p>To achieve this, let's create a utility module inside <code>src/lib/utils.ts</code> to build the required WAV header, convert raw audio samples into a binary buffer, and write it to local storage.</p>
<pre><code class="language-typescript">import { Buffer } from "buffer";
import * as FileSystem from "expo-file-system/legacy";

/**
 * Creates a WAV header for 16-bit PCM audio
 */
export function createWavHeader(
  dataLength: number,
  sampleRate: number,
): Buffer {
  const buffer = Buffer.alloc(44);
  const channels = 1; // Mono
  const byteRate = sampleRate * channels * 2; // 16-bit audio
  const blockAlign = channels * 2;

  buffer.write("RIFF", 0);
  buffer.writeUInt32LE(36 + dataLength, 4);
  buffer.write("WAVE", 8);
  buffer.write("fmt ", 12);
  buffer.writeUInt32LE(16, 16); // Subchunk1Size
  buffer.writeUInt16LE(1, 20); // AudioFormat (PCM)
  buffer.writeUInt16LE(channels, 22);
  buffer.writeUInt32LE(sampleRate, 24);
  buffer.writeUInt32LE(byteRate, 28);
  buffer.writeUInt16LE(blockAlign, 32);
  buffer.writeUInt16LE(16, 34); // BitsPerSample
  buffer.write("data", 36);
  buffer.writeUInt32LE(dataLength, 40);

  return buffer;
}

/**
 * Converts the raw Int16Array samples from QVAC to a binary Buffer
 */
export function int16ArrayToBuffer(int16Array: Int16Array): Buffer {
  const buffer = Buffer.alloc(int16Array.length * 2);
  for (let i = 0; i &lt; int16Array.length; i++) {
    buffer.writeInt16LE(int16Array[i] ?? 0, i * 2);
  }
  return buffer;
}

/**
 * Main function to package and save the file to local mobile storage
 */
export async function saveAudioToDevice(
  audioBuffer: Int16Array,
  sampleRate: number,
): Promise&lt;string&gt; {
  try {
    const audioData = int16ArrayToBuffer(audioBuffer);
    const wavHeader = createWavHeader(audioData.length, sampleRate);
    const finalWavBuffer = Buffer.concat([wavHeader, audioData]);
    const base64Data = finalWavBuffer.toString("base64");

    const filename = `tts-speech-${Date.now()}.wav`;
    const fileUri = `\({FileSystem.documentDirectory}\){filename}`;

    await FileSystem.writeAsStringAsync(fileUri, base64Data, {
      encoding: FileSystem.EncodingType.Base64,
    });

    console.log(`✅ File saved locally at: ${fileUri}`);
    return fileUri;
  } catch (error) {
    console.error("❌ Failed to save audio file locally:", error);
    throw error;
  }
}
</code></pre>
<h2 id="heading-complete-implementation">Complete Implementation</h2>
<p>Let's bring it all together. We'll implement an interface that takes user input, manages download and loading states for the Supertonic engine, packages generated raw waves into a playable local file, and renders an interactive visual waveform player.</p>
<p>Replace your entry app file <code>src/app/index.tsx</code> with the following implementation:</p>
<pre><code class="language-tsx">import { useState, useEffect } from "react";
import {
  TextInput,
  KeyboardAvoidingView,
  Platform,
  ScrollView,
} from "react-native";
import {
  loadModel,
  unloadModel,
  textToSpeech,
  downloadAsset,
  TTS_EN_SUPERTONIC_Q8_0,
  getModelInfo,
  type ModelProgressUpdate,
} from "@qvac/sdk";
import { saveAudioToDevice } from "@/lib/utils";
import { TtsModelLoader } from "@/components/tts-model-loader";
import { AudioPlayer } from "@/components/audio-player";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Text } from "@/components/ui/text";

const SUPERTONIC_SAMPLE_RATE = 44100;

// Global reference for our model ID
let globalModelId: string | null = null;

type TtsStatus =
  | { phase: "idle" }
  | { phase: "synthesizing" }
  | { phase: "done"; audioUri: string }
  | { phase: "error"; message: string };

export default function TextToVoiceScreen() {
  const [text, setText] = useState("");
  const [status, setStatus] = useState&lt;TtsStatus&gt;({ phase: "idle" });

  const [isModelLoaded, setIsModelLoaded] = useState(!!globalModelId);
  const [isDownloading, setIsDownloading] = useState(false);
  const [downloadProgress, setDownloadProgress] = useState(0);

  const isBusy = status.phase === "synthesizing";

  useEffect(() =&gt; {
    async function checkAndAutoLoad() {
      if (globalModelId) return;
      try {
        const info = await getModelInfo({ name: TTS_EN_SUPERTONIC_Q8_0.name });
        if (info.isCached) {
          setIsDownloading(true);
          setDownloadProgress(1);

          globalModelId = await loadModel({
            modelSrc: TTS_EN_SUPERTONIC_Q8_0,
            modelConfig: {
              ttsEngine: "supertonic",
              language: "en",
              voice: "F1",
              ttsSpeed: 1.05,
              ttsNumInferenceSteps: 5,
            },
          });

          setIsModelLoaded(true);
          setIsDownloading(false);
        }
      } catch (err: unknown) {
        console.warn("Failed to auto-load cached model on mount:", err);
        setIsDownloading(false);
      }
    }
    checkAndAutoLoad();
  }, []);

  const handleDownloadModel = async () =&gt; {
    if (isDownloading || isModelLoaded) return;

    try {
      setIsDownloading(true);
      setDownloadProgress(0);

      await downloadAsset({
        assetSrc: TTS_EN_SUPERTONIC_Q8_0,
        onProgress: (p: ModelProgressUpdate) =&gt; {
          setDownloadProgress(p.percentage / 100);
        },
      });

      setDownloadProgress(1);

      globalModelId = await loadModel({
        modelSrc: TTS_EN_SUPERTONIC_Q8_0,
        modelConfig: {
          ttsEngine: "supertonic",
          language: "en",
          voice: "F1",
          ttsSpeed: 1.05,
          ttsNumInferenceSteps: 5,
        },
      });

      setIsModelLoaded(true);
      setIsDownloading(false);
    } catch (err: unknown) {
      console.error("Failed to download or load model:", err);
      setIsDownloading(false);
      setStatus({
        phase: "error",
        message: err instanceof Error ? err.message : String(err),
      });
      setIsModelLoaded(false);
    }
  };

  const handleSubmit = async () =&gt; {
    if (!text.trim() || isBusy || !globalModelId) return;

    try {
      setStatus({ phase: "synthesizing" });

      // 1. Unload and reload the model to reset its state and clear the KV cache.
      if (globalModelId) {
        await unloadModel({ modelId: globalModelId });
      }
      globalModelId = await loadModel({
        modelSrc: TTS_EN_SUPERTONIC_Q8_0,
        modelConfig: {
          ttsEngine: "supertonic",
          language: "en",
          voice: "F1",
          ttsSpeed: 1.05,
          ttsNumInferenceSteps: 5,
        },
      });

      // 2. Synthesize text to raw PCM samples
      const result = textToSpeech({
        modelId: globalModelId,
        text: text.trim(),
        inputType: "text",
        stream: false,
      });

      const audioBuffer = await result.buffer;

      // 3. Package and save WAV file using our local util
      const samplesInt16 = new Int16Array(audioBuffer);
      const wavUri = await saveAudioToDevice(
        samplesInt16,
        SUPERTONIC_SAMPLE_RATE,
      );

      // 4. Show player
      setStatus({ phase: "done", audioUri: wavUri });
    } catch (err: unknown) {
      console.error("TTS error:", err);
      const msg = err instanceof Error ? err.message : String(err);
      setStatus({ phase: "error", message: msg });
    }
  };

  const buttonLabel =
    status.phase === "synthesizing" ? "Synthesizing…" : "Synthesize Speech";

  if (!isModelLoaded) {
    return (
      &lt;TtsModelLoader
        onDownload={handleDownloadModel}
        isDownloading={isDownloading}
        progress={downloadProgress}
      /&gt;
    );
  }

  return (
    &lt;KeyboardAvoidingView
      behavior={Platform.OS === "ios" ? "padding" : "height"}
      className="flex-1 bg-black"
    &gt;
      &lt;ScrollView contentContainerClassName="flex-grow p-6  justify-center"&gt;
        &lt;Card className="border border-border bg-card max-w-md w-full mx-auto"&gt;
          &lt;CardHeader&gt;
            &lt;CardTitle variant="h3" className="text-white text-center"&gt;
              Text to Voice
            &lt;/CardTitle&gt;
            &lt;CardDescription className="text-center mt-1"&gt;
              Type or paste your content to synthesize speech
            &lt;/CardDescription&gt;
          &lt;/CardHeader&gt;

          &lt;CardContent className="gap-6"&gt;
            &lt;TextInput
              className="bg-muted text-white border border-border rounded-lg p-4 h-48 text-base leading-6"
              multiline
              numberOfLines={8}
              placeholder="Type your message here..."
              placeholderTextColor="#666"
              value={text}
              onChangeText={setText}
              style={{ textAlignVertical: "top" }}
              editable={!isBusy}
            /&gt;

            {status.phase === "error" &amp;&amp; (
              &lt;Text className="text-destructive text-sm text-center"&gt;
                {status.message}
              &lt;/Text&gt;
            )}

            {status.phase === "done" &amp;&amp; &lt;AudioPlayer uri={status.audioUri} /&gt;}

            &lt;Button
              onPress={handleSubmit}
              className="w-full h-12 rounded-xl"
              disabled={!text.trim() || isBusy}
            &gt;
              &lt;Text className="font-semibold text-lg"&gt;{buttonLabel}&lt;/Text&gt;
            &lt;/Button&gt;
          &lt;/CardContent&gt;
        &lt;/Card&gt;
      &lt;/ScrollView&gt;
    &lt;/KeyboardAvoidingView&gt;
  );
}
</code></pre>
<h3 id="heading-codebase-breakdown">Codebase Breakdown</h3>
<p>Let’s lift the hood on how this local Text-to-Speech implementation manages native model lifecycles and processes raw audio arrays.</p>
<h4 id="heading-1-managing-the-native-lifecycle">1. Managing the Native Lifecycle</h4>
<p>Loading neural network weights for speech synthesis is computationally expensive. When the QVAC runtime initializes a model, it must read parameters from the local disk and copy the active weights into device RAM.</p>
<p>To handle this efficiently, we declared the reference variable outside the component scope:</p>
<pre><code class="language-typescript">let globalModelId: string | null = null;
</code></pre>
<p>If <code>globalModelId</code> were tracked inside component states, navigating away from the text-to-speech screen would clean up the state, causing the app to unnecessarily drop the reference. Storing the ID globally ensures we hold onto it across layout transitions.</p>
<h4 id="heading-2-flushing-the-kv-cache-unload-and-reload">2. Flushing the KV Cache: Unload and Reload</h4>
<p>One of the most important aspects of offline generation using GGML engines is state management:</p>
<pre><code class="language-typescript">// 1. Unload and reload the model to reset its state and clear the KV cache.
if (globalModelId) {
  await unloadModel({ modelId: globalModelId });
}

globalModelId = await loadModel({ ... });
</code></pre>
<p>WARNING about <strong>acoustic hallucinations:</strong> If you continuously synthesize sentences on a single TTS model instance without resetting it, the model's Key-Value (KV) cache fills up. It begins treating your new sentence as a continuation of the previous one, leading to heavy robotic distortion, echoing, and repeated voices.</p>
<p>By explicitly destroying the model via <code>unloadModel</code> and immediately booting a fresh instance with <code>loadModel</code>, we're forcing a pristine, empty context window. Since the model is already downloaded and memory-mapped, reloading the model directly from local flash storage is extremely fast, typically completing in a fraction of a second on modern mobile hardware to ensure a seamless user experience while guaranteeing artifact-free audio.</p>
<h4 id="heading-3-demystifying-the-wav-header-structure">3. Demystifying the WAV Header Structure</h4>
<p>Operating systems and built-in mobile media decoders are unable to parse raw, naked PCM (Pulse Code Modulation) sound waves directly. A raw PCM buffer is simply a stream of numerical coordinates representing audio wave amplitudes.</p>
<p>We resolve this by prepending-formatting our PCM buffer with a standard 44-byte RIFF/WAVE header.</p>
<p>This header acts as a passport, defining:</p>
<ul>
<li><p><strong>AudioFormat (</strong><code>1</code><strong>)</strong>: Signals uncompressed linear PCM.</p>
</li>
<li><p><strong>NumChannels (</strong><code>1</code><strong>)</strong>: Mono audio.</p>
</li>
<li><p><strong>SampleRate (</strong><code>44100</code><strong>)</strong>: The clock frequency required for Supertonic playback.</p>
</li>
<li><p><strong>BitsPerSample (</strong><code>16</code><strong>)</strong>: 16-bit word length (2 bytes per sample).</p>
</li>
</ul>
<p>Additionally, writing the file is handled via Base64 encoding to safely cross React Native's JavaScript-to-Native bridge without dropping binary data:</p>
<pre><code class="language-typescript">const base64Data = finalWavBuffer.toString("base64");
await FileSystem.writeAsStringAsync(fileUri, base64Data, {
  encoding: FileSystem.EncodingType.Base64,
});
</code></pre>
<h4 id="heading-4-visual-waveform-player">4. Visual Waveform Player</h4>
<p>Rather than using a basic headless native audio player that fires immediately in the background, we pass the local WAV file path to a custom <code>&lt;AudioPlayer&gt;</code> component powered by <code>@simform_solutions/react-native-audio-waveform</code>.</p>
<p>This module analyzes our newly written WAV file and draws a sleek, WhatsApp-inspired interactive visual waveform, giving the user full control over playback, dynamic speed adjustments (<code>1x</code>, <code>1.5x</code>, <code>2x</code>), and seeking. It's a vast UX improvement that makes the final result feel premium and polished.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Transitioning Text-to-Speech from the cloud to on-device hardware offers a practical approach for mobile application developers. Running model inference locally eliminates reliance on remote internet connectivity, removes recurring API usage costs, and ensures that user text inputs never leave the physical device.</p>
<p>Integrating local speech synthesis can be highly beneficial for interactive, educational, or conversational apps. For example, in voice-guided systems, on-device TTS allows applications to function in private or offline environments. As edge processors gain dedicated hardware acceleration cores and open-source models decrease in memory size through quantization research, local-first architectures present a compelling alternative for developers prioritizing privacy, offline resilience, and predictable cost structures.</p>
<h2 id="heading-resources-and-further-reading">Resources and Further Reading</h2>
<p>To dive deeper into local Text-to-Speech inference, inspect the source code, or explore advanced configurations for your mobile applications, check out the following resources:</p>
<ul>
<li><p><a href="https://docs.qvac.tether.io/tutorials/expo/"><strong>QVAC Expo Integration Docs</strong></a>: Learn more about configuring custom local models in Expo.</p>
</li>
<li><p><a href="https://github.com/SimformSolutionsPvtLtd/react-native-audio-waveform"><strong>react-native-audio-waveform</strong></a>: Learn more about interactive React Native audio visualizations.</p>
</li>
<li><p><a href="https://huggingface.co/models?search=gguf"><strong>GGUF Model Hub on Hugging Face</strong></a>: Browse compatible quantized open-source models.</p>
</li>
<li><p><a href="https://www.emergentmind.com/topics/latent-denoising-diffusion-models"><strong>Latent Denoising Deep Dive</strong></a>: Technical deep dive into Diffusion-based acoustic generation.</p>
</li>
<li><p><a href="https://github.com/DjibrilM/QVAC-TTS-Expo-Implementation"><strong>https://github.com/DjibrilM/QVAC-TTS-Expo-Implementation</strong></a>: Full implementation code.</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Create Fluid Animations with React Native Reanimated v4 ]]>
                </title>
                <description>
                    <![CDATA[ Reanimated 4 brings Cascading Style Sheets (CSS) animations to React Native while keeping full backward compatibility with its worklet-based API. You can now build 60+ frames-per-second (FPS) animations using familiar web syntax, or drop down to work... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-create-fluid-animations-with-react-native-reanimated-v4/</link>
                <guid isPermaLink="false">691b3eab5aa173ac953652e2</guid>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ react native reanimated ]]>
                    </category>
                
                    <category>
                        <![CDATA[ animation ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Balogun Wahab ]]>
                </dc:creator>
                <pubDate>Mon, 17 Nov 2025 15:26:35 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763052228638/4416e81d-b76e-4c40-987e-0aff1d82ff7b.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Reanimated 4 brings Cascading Style Sheets (CSS) animations to React Native while keeping full backward compatibility with its worklet-based API. You can now build 60+ frames-per-second (FPS) animations using familiar web syntax, or drop down to worklets for gesture-driven interactions.</p>
<p>The library requires React Native's New Architecture (Fabric), so you'll need version 0.76 or newer.</p>
<p>In this tutorial, you'll learn:</p>
<ul>
<li><p>How to use CSS transitions for state-driven animations</p>
</li>
<li><p>When to use worklets for gesture and scroll interactions</p>
</li>
<li><p>How to migrate from Reanimated 3 to 4</p>
</li>
<li><p>Practical patterns for collapsing headers, bottom sheets, and carousels</p>
</li>
<li><p>Performance optimization techniques</p>
</li>
</ul>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>You should have:</p>
<ul>
<li><p>React Native 0.76+ with New Architecture enabled</p>
</li>
<li><p>Basic React hooks knowledge (useState, useEffect)</p>
</li>
<li><p>Node.js and npm or yarn are installed</p>
</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-installation-and-setup">Installation and Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-the-two-approaches">Understanding the Two Approaches (CSS Animations and Worklets)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-migrate-from-reanimated-version-3-to-version-4">How to Migrate from Reanimated Version 3 to Version 4</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-css-animations-tutorial">CSS Animations Tutorial</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-worklets-tutorial">Worklets Tutorial</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-real-world-patterns">Real-World Patterns</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-performance-optimizations">Performance Optimizations</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-debugging-tips">Debugging Tips</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-installation-and-setup">Installation and Setup</h2>
<p>To get started, you'll need to install the required packages:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># For Expo</span>
npx expo install react-native-reanimated react-native-worklets

<span class="hljs-comment"># For React Native CLI  </span>
npm install react-native-reanimated react-native-worklets
<span class="hljs-built_in">cd</span> ios &amp;&amp; pod install &amp;&amp; <span class="hljs-built_in">cd</span> ..
</code></pre>
<p>Update <code>babel.config.js</code> (the plugin must be last):</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">presets</span>: [<span class="hljs-string">'module:metro-react-native-babel-preset'</span>],
  <span class="hljs-attr">plugins</span>: [
    <span class="hljs-string">'react-native-worklets/plugin'</span>, <span class="hljs-comment">// Must be last</span>
  ],
};
</code></pre>
<p>Then clear the cache and rebuild:</p>
<pre><code class="lang-bash">npm start -- --reset-cache
npx react-native run-ios
</code></pre>
<h2 id="heading-understanding-the-two-approaches">Understanding the Two Approaches</h2>
<p>React Native Reanimated is an animation library that runs animations on the native thread instead of the JavaScript thread. This means your animations stay smooth even when your JavaScript code is busy processing data or handling user interactions.</p>
<p>Unlike React Native's built-in Animated API, Reanimated executes animation logic directly on the UI thread. This eliminates the performance bottleneck caused by communication between JavaScript and native code, which enables Reanimated to maintain 60 FPS even during complex operations.</p>
<p>Reanimated 4 offers two animation systems, each designed for different use cases.</p>
<h3 id="heading-css-animations">CSS Animations</h3>
<p>CSS animations work declaratively, meaning you describe what you want to happen rather than how to make it happen. You define which properties should animate (like width, color, or opacity), specify the timing and easing, then simply change the values through React state updates. Reanimated automatically handles the animation between the old and new values.</p>
<p>This approach excels at predictable, state-driven animations where you know both the starting and ending states. It's ideal for:</p>
<ul>
<li><p>Showing and hiding UI elements (modals, tooltips, notifications)</p>
</li>
<li><p>Expanding and collapsing content (accordions, dropdown menus)</p>
</li>
<li><p>Visual feedback for state changes (button hover effects, selection highlights)</p>
</li>
<li><p>Loading indicators and progress animations</p>
</li>
<li><p>Color and opacity transitions</p>
</li>
</ul>
<h3 id="heading-worklets">Worklets</h3>
<p>Worklets take a different approach by giving you imperative, frame-by-frame control over animations. They run on the UI thread and use "shared values" – special variables that can be accessed and modified from both JavaScript and native code without any communication overhead.</p>
<p>Worklets are essential for interactive animations that need to respond in real-time to user input or continuous data streams. They're best for:</p>
<ul>
<li><p>Gesture-driven interactions (drag-and-drop, swipe-to-dismiss, pinch-to-zoom)</p>
</li>
<li><p>Scroll-linked effects (parallax images, collapsing headers, sticky elements)</p>
</li>
<li><p>Physics-based animations (spring effects, momentum scrolling)</p>
</li>
<li><p>Sensor-based animations (responding to device orientation)</p>
</li>
<li><p>Any animation requiring dynamic, real-time control</p>
</li>
</ul>
<p>Now that you understand the two approaches Reanimated offers, let's look at how to migrate from version 3 if you're already using the library.</p>
<h2 id="heading-how-to-migrate-from-reanimated-version-3-to-version-4">How to Migrate from Reanimated Version 3 to Version 4</h2>
<p>If you're currently using Reanimated 3, you'll be happy to know that version 4 maintains backward compatibility. Your existing animations using worklets, shared values, and <code>useAnimatedStyle</code> will continue to work without modification.</p>
<p>But version 4 introduces some architectural changes and removes deprecated APIs, so you'll need to make a few updates to your project configuration and code. Let's walk through the migration process step by step.</p>
<h3 id="heading-what-changed-in-version-4">What Changed in Version 4</h3>
<p>The most significant change is that worklets have been extracted into a separate package called <code>react-native-worklets-core</code>. This modular approach allows other libraries beyond Reanimated to leverage worklet functionality.</p>
<p>Because of this separation, you'll need to update your Babel configuration. Change the plugin from <code>react-native-reanimated/plugin</code> to <code>react-native-worklets/plugin</code>.</p>
<p>Version 4 also exclusively supports React Native's New Architecture (Fabric). The old Paper renderer is no longer compatible. If your project hasn't migrated to the New Architecture yet, you'll need to either upgrade to React Native 0.76+ (which has New Architecture enabled by default) or stay on Reanimated 3.x until you're ready to make that transition.</p>
<h3 id="heading-removed-apis">Removed APIs</h3>
<p>Several APIs that were deprecated in version 3 have been removed in version 4. Here's what you need to replace:</p>
<ul>
<li><p><code>useAnimatedGestureHandler</code> → Use the <code>Gesture</code> API from react-native-gesture-handler 2.x instead</p>
</li>
<li><p><code>useWorkletCallback</code> → Use <code>useCallback</code> with the <code>'worklet'</code> directive</p>
</li>
<li><p><code>combineTransition</code> → Use <code>EntryExitTransition.entering().exiting()</code></p>
</li>
</ul>
<p>The <code>useScrollViewOffset</code> hook has been renamed to <code>useScrollOffset</code>. The old name still works but is deprecated, so update your code to use the new name.</p>
<h3 id="heading-spring-configuration-change">Spring Configuration Change</h3>
<p>The spring animation configuration has changed to feel more natural. The <code>duration</code> parameter now represents "perceptual duration" rather than exact milliseconds. The actual animation runs approximately 1.5 times longer than the specified duration, creating springs that feel more organic and less mechanical.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Version 3</span>
withSpring(<span class="hljs-number">100</span>, { <span class="hljs-attr">duration</span>: <span class="hljs-number">300</span> }) <span class="hljs-comment">// Runs for exactly 300ms</span>

<span class="hljs-comment">// Version 4  </span>
withSpring(<span class="hljs-number">100</span>, { <span class="hljs-attr">duration</span>: <span class="hljs-number">200</span> }) <span class="hljs-comment">// Runs for approximately 300ms</span>
</code></pre>
<p>If you need to maintain the exact timing from version 3, divide your duration values by 1.5.</p>
<h3 id="heading-step-by-step-migration-process">Step-by-Step Migration Process</h3>
<p>Here's how to migrate your project from Reanimated 3 to version 4:</p>
<p><strong>Step 1:</strong> Verify your project is using React Native 0.76 or newer with New Architecture enabled. Check your iOS Podfile for <code>ENV['RCT_NEW_ARCH_ENABLED'] = '1'</code> and your Android gradle.properties for <code>newArchEnabled=true</code>.</p>
<p><strong>Step 2:</strong> Install the new versions of Reanimated and the worklets package:</p>
<pre><code class="lang-bash">npm install react-native-reanimated@^4.1.0 react-native-worklets@^0.5.0
</code></pre>
<p><strong>Step 3:</strong> Update your <code>babel.config.js</code> to use the new worklets plugin:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">plugins</span>: [
    <span class="hljs-string">'react-native-worklets/plugin'</span>, <span class="hljs-comment">// Changed from react-native-reanimated/plugin</span>
  ],
};
</code></pre>
<p><strong>Step 4:</strong> Search your codebase for the removed APIs and replace them:</p>
<ul>
<li><p>Replace <code>useAnimatedGestureHandler</code> with the <code>Gesture</code> API</p>
</li>
<li><p>Replace <code>useWorkletCallback</code> with <code>useCallback</code> and add <code>'worklet'</code> directive</p>
</li>
<li><p>Replace <code>combineTransition</code> with <code>EntryExitTransition.entering().exiting()</code></p>
</li>
<li><p>Rename <code>useScrollViewOffset</code> to <code>useScrollOffset</code></p>
</li>
</ul>
<p><strong>Step 5:</strong> Rebuild your native apps:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> ios &amp;&amp; pod install &amp;&amp; <span class="hljs-built_in">cd</span> ..
npx react-native run-ios
<span class="hljs-comment"># or for Android</span>
npx react-native run-android
</code></pre>
<p>After completing these steps, your app should be running on Reanimated 4 with all your existing animations working as before. You're now ready to start using the new CSS animation features alongside your existing worklet-based animations. In the next section, you'll learn how to build animations using the CSS syntax.</p>
<h2 id="heading-css-animations-tutorial">CSS Animations Tutorial</h2>
<p>CSS animations provide a clean, declarative way to handle transitions that are triggered by state changes. Instead of manually managing animation values, you simply declare which properties should animate and how, then update your component state – Reanimated handles the rest.</p>
<p>This approach is particularly powerful for animations where you know the start and end states ahead of time. It's perfect for UI elements that toggle between different visual states, like modals appearing and disappearing, buttons providing feedback on press, or content expanding and collapsing.</p>
<h3 id="heading-basic-transitions">Basic Transitions</h3>
<p>A transition animates the change between two property values. When you specify a property that should transition, Reanimated automatically interpolates between the old and new values over the specified duration.</p>
<p>Let's look at an expandable card that grows when tapped:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> React, { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { Pressable, Text } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native'</span>;
<span class="hljs-keyword">import</span> Animated <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native-reanimated'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ExpandableCard</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [expanded, setExpanded] = useState(<span class="hljs-literal">false</span>);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Pressable</span> <span class="hljs-attr">onPress</span>=<span class="hljs-string">{()</span> =&gt;</span> setExpanded(!expanded)}&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span>
        <span class="hljs-attr">width:</span> <span class="hljs-attr">expanded</span> ? <span class="hljs-attr">300</span> <span class="hljs-attr">:</span> <span class="hljs-attr">200</span>,
        <span class="hljs-attr">height:</span> <span class="hljs-attr">expanded</span> ? <span class="hljs-attr">200</span> <span class="hljs-attr">:</span> <span class="hljs-attr">100</span>,
        <span class="hljs-attr">backgroundColor:</span> <span class="hljs-attr">expanded</span> ? '#<span class="hljs-attr">4ade80</span>' <span class="hljs-attr">:</span> '#<span class="hljs-attr">86efac</span>',
        <span class="hljs-attr">transitionProperty:</span> ['<span class="hljs-attr">width</span>', '<span class="hljs-attr">height</span>', '<span class="hljs-attr">backgroundColor</span>'],
        <span class="hljs-attr">transitionDuration:</span> <span class="hljs-attr">300</span>,
        <span class="hljs-attr">transitionTimingFunction:</span> '<span class="hljs-attr">ease-in-out</span>',
      }}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Text</span>&gt;</span>Tap to toggle<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.View</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Pressable</span>&gt;</span></span>
  );
}
</code></pre>
<p>Here's what's happening: The card's width, height, and background color are controlled by the <code>expanded</code> state. The <code>transitionProperty</code> array tells Reanimated which properties to animate. When <code>expanded</code> changes, Reanimated smoothly animates from the current values to the new values over 300 milliseconds, using an ease-in-out timing function that starts slow, speeds up, then slows down again.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762274975831/7e25aa04-24b8-43eb-9db0-a3c088c44132.gif" alt="7e25aa04-24b8-43eb-9db0-a3c088c44132" class="image--center mx-auto" width="295" height="640" loading="lazy"></p>
<h3 id="heading-keyframe-animations">Keyframe Animations</h3>
<p>While transitions handle changes between two states, keyframe animations let you define multi-step sequences with precise control over each stage. You create an object where each key represents a percentage of the animation timeline, and the value defines what properties should look like at that point.</p>
<p>Here's a pulsing badge that scales up and fades slightly, then returns to normal:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> pulseAnimation = {
  <span class="hljs-string">'0%'</span>: { <span class="hljs-attr">scale</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">opacity</span>: <span class="hljs-number">1</span> },
  <span class="hljs-string">'50%'</span>: { <span class="hljs-attr">scale</span>: <span class="hljs-number">1.05</span>, <span class="hljs-attr">opacity</span>: <span class="hljs-number">0.8</span> },
  <span class="hljs-string">'100%'</span>: { <span class="hljs-attr">scale</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">opacity</span>: <span class="hljs-number">1</span> },
};

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">PulsingBadge</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span>
      <span class="hljs-attr">width:</span> <span class="hljs-attr">50</span>,
      <span class="hljs-attr">height:</span> <span class="hljs-attr">50</span>,
      <span class="hljs-attr">borderRadius:</span> <span class="hljs-attr">25</span>,
      <span class="hljs-attr">backgroundColor:</span> '#<span class="hljs-attr">ef4444</span>',
      <span class="hljs-attr">animationName:</span> <span class="hljs-attr">pulseAnimation</span>,
      <span class="hljs-attr">animationDuration:</span> <span class="hljs-attr">2000</span>,
      <span class="hljs-attr">animationIterationCount:</span> '<span class="hljs-attr">infinite</span>',
    }} /&gt;</span></span>
  );
}
</code></pre>
<p>The animation starts at 0% (normal size and opacity), grows and fades at the 50% mark, then returns to the original state at 100%. By setting <code>animationIterationCount</code> to 'infinite', the animation loops continuously. This creates the pulsing effect you often see on notification badges or live indicators.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762274903470/d10d856f-03b8-4d6c-b616-22cbea3434c2.gif" alt="d10d856f-03b8-4d6c-b616-22cbea3434c2" class="image--center mx-auto" width="295" height="640" loading="lazy"></p>
<h3 id="heading-built-in-animations">Built-in Animations</h3>
<p>Reanimated includes a collection of pre-built animations for common entrance and exit effects. These save you from writing animation configurations for standard patterns like fading, sliding, and zooming.</p>
<p>Here's a modal that fades in when shown and fades out when hidden:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { FadeIn, FadeOut } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native-reanimated'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Modal</span>(<span class="hljs-params">{ visible, children }</span>) </span>{
  <span class="hljs-keyword">if</span> (!visible) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> 
      <span class="hljs-attr">entering</span>=<span class="hljs-string">{FadeIn.duration(300)}</span>
      <span class="hljs-attr">exiting</span>=<span class="hljs-string">{FadeOut.duration(200)}</span>
    &gt;</span>
      {children}
    <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.View</span>&gt;</span></span>
  );
}
</code></pre>
<p>The <code>entering</code> prop automatically applies the fade-in animation when the component mounts, and <code>exiting</code> applies the fade-out before unmounting. Other commonly used built-in animations include <code>SlideInRight</code>, <code>SlideOutLeft</code> (for drawer-style entrances), and <code>ZoomIn</code>, <code>ZoomOut</code> (for attention-grabbing pop-ins).</p>
<p>Now that you understand CSS animations for state-driven transitions, let's explore worklets for creating interactive animations that respond to user input in real-time.</p>
<h2 id="heading-worklets-tutorial">Worklets Tutorial</h2>
<p>While CSS animations excel at predefined state transitions, many animations need to respond dynamically to user input. This is where worklets come in. Worklets give you frame-by-frame control over animations, allowing them to follow gestures, scroll position, or any other real-time input source.</p>
<p>Interactive animations differ from CSS animations in that they don't have predefined start and end states. Instead, they continuously update based on user input. For example, a draggable element needs to follow your finger precisely as you move it – there's no way to know ahead of time where you'll drag it. This requires imperative control, where you directly manipulate animation values in response to events.</p>
<h3 id="heading-basic-worklet-animation">Basic Worklet Animation</h3>
<p>Shared values are the foundation of worklet-based animations. They're special variables that exist simultaneously in both the JavaScript and UI threads, allowing you to update them from JavaScript while the UI thread reads them to update the display – all without any communication overhead.</p>
<p>Here's a button that scales down when pressed and bounces back when released:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> Animated, { 
  useSharedValue, 
  useAnimatedStyle, 
  withSpring 
} <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native-reanimated'</span>;
<span class="hljs-keyword">import</span> { Pressable } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">BouncyButton</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> scale = useSharedValue(<span class="hljs-number">1</span>);

  <span class="hljs-keyword">const</span> animatedStyle = useAnimatedStyle(<span class="hljs-function">() =&gt;</span> ({
    <span class="hljs-attr">transform</span>: [{ <span class="hljs-attr">scale</span>: scale.value }],
  }));

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Pressable</span>
      <span class="hljs-attr">onPressIn</span>=<span class="hljs-string">{()</span> =&gt;</span> { scale.value = withSpring(0.9); }}
      onPressOut={() =&gt; { scale.value = withSpring(1); }}
    &gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{[styles.button,</span> <span class="hljs-attr">animatedStyle</span>]}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Text</span>&gt;</span>Press Me<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.View</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Pressable</span>&gt;</span></span>
  );
}
</code></pre>
<p>The <code>useSharedValue(1)</code> creates a shared value initialized to 1 (normal scale). The <code>useAnimatedStyle</code> hook creates a style object that depends on this shared value and runs on the UI thread. When you press the button, <code>scale.value = withSpring(0.9)</code> updates the shared value, and <code>withSpring</code> creates a spring animation to the new value. The <code>useAnimatedStyle</code> hook automatically re-runs, updating the transform with the new scale value.</p>
<h3 id="heading-gesture-animations">Gesture Animations</h3>
<p>Gestures require even tighter integration between user input and animation. The react-native-gesture-handler library provides high-performance gesture recognition that works seamlessly with Reanimated.</p>
<p>First, install the gesture handler:</p>
<pre><code class="lang-bash">npm install react-native-gesture-handler
<span class="hljs-built_in">cd</span> ios &amp;&amp; pod install &amp;&amp; <span class="hljs-built_in">cd</span> ..
</code></pre>
<p>Next, you need to wrap your app with <code>GestureHandlerRootView</code>. This component sets up the gesture handling system at the root of your application. Without it, gestures won't work. Think of it as activating the gesture system for your entire app:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { GestureHandlerRootView } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native-gesture-handler'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">GestureHandlerRootView</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">flex:</span> <span class="hljs-attr">1</span> }}&gt;</span>
      {/* Your app content goes here */}
    <span class="hljs-tag">&lt;/<span class="hljs-name">GestureHandlerRootView</span>&gt;</span></span>
  );
}
</code></pre>
<p>Now you can create gesture-driven animations. Here's a box you can drag around the screen that springs back to center when released:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { Gesture, GestureDetector } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native-gesture-handler'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">DraggableBox</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> offsetX = useSharedValue(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">const</span> offsetY = useSharedValue(<span class="hljs-number">0</span>);

  <span class="hljs-keyword">const</span> pan = Gesture.Pan()
    .onChange(<span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      offsetX.value += event.changeX;
      offsetY.value += event.changeY;
    })
    .onEnd(<span class="hljs-function">() =&gt;</span> {
      offsetX.value = withSpring(<span class="hljs-number">0</span>);
      offsetY.value = withSpring(<span class="hljs-number">0</span>);
    });

  <span class="hljs-keyword">const</span> animatedStyle = useAnimatedStyle(<span class="hljs-function">() =&gt;</span> ({
    <span class="hljs-attr">transform</span>: [
      { <span class="hljs-attr">translateX</span>: offsetX.value },
      { <span class="hljs-attr">translateY</span>: offsetY.value },
    ],
  }));

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">GestureDetector</span> <span class="hljs-attr">gesture</span>=<span class="hljs-string">{pan}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{[styles.box,</span> <span class="hljs-attr">animatedStyle</span>]} /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">GestureDetector</span>&gt;</span></span>
  );
}
</code></pre>
<p>The <code>Gesture.Pan()</code> creates a pan gesture recognizer. The <code>.onChange()</code> callback fires continuously while you're dragging – <code>event.changeX</code> and <code>event.changeY</code> tell you how much the finger moved since the last frame. By adding these values to the offsets, the box follows your finger. When you lift your finger, <code>.onEnd()</code> fires and springs the box back to the center (0, 0).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762275456257/3a536406-d678-46eb-9cfe-f689428d3412.gif" alt="3a536406-d678-46eb-9cfe-f689428d3412" class="image--center mx-auto" width="295" height="640" loading="lazy"></p>
<h3 id="heading-scroll-linked-animations">Scroll-Linked Animations</h3>
<p>Another common use case for worklets is creating effects that respond to scroll position, like headers that shrink as you scroll down or parallax backgrounds.</p>
<p>Here's a header that collapses as you scroll:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ParallaxHeader</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> scrollY = useSharedValue(<span class="hljs-number">0</span>);

  <span class="hljs-keyword">const</span> scrollHandler = useAnimatedScrollHandler({
    <span class="hljs-attr">onScroll</span>: <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      scrollY.value = event.contentOffset.y;
    },
  });

  <span class="hljs-keyword">const</span> headerStyle = useAnimatedStyle(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> height = interpolate(
      scrollY.value,
      [<span class="hljs-number">0</span>, <span class="hljs-number">150</span>],
      [<span class="hljs-number">200</span>, <span class="hljs-number">60</span>],
      <span class="hljs-string">'clamp'</span>
    );

    <span class="hljs-keyword">return</span> { height };
  });

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{[styles.header,</span> <span class="hljs-attr">headerStyle</span>]}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Text</span>&gt;</span>Header<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.View</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.ScrollView</span>
        <span class="hljs-attr">onScroll</span>=<span class="hljs-string">{scrollHandler}</span>
        <span class="hljs-attr">scrollEventThrottle</span>=<span class="hljs-string">{16}</span>
      &gt;</span>
        {/* Content */}
      <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.ScrollView</span>&gt;</span>
    <span class="hljs-tag">&lt;/&gt;</span></span>
  );
}
</code></pre>
<p>The <code>useAnimatedScrollHandler</code> creates a scroll event handler that runs on the UI thread. Every time you scroll, it updates <code>scrollY</code> with the current scroll position. The <code>interpolate</code> function maps the scroll position to the header height – when scrollY is 0 (top of the scroll), height is 200. When scrollY reaches 150, height is 60. The 'clamp' option prevents the height from going outside this range.</p>
<p>With these fundamentals of CSS animations and worklets covered, let's look at how to apply them to common real-world scenarios.</p>
<h2 id="heading-real-world-patterns">Real-World Patterns</h2>
<p>Now that you understand both CSS animations and worklets, let's combine them to build three patterns you'll frequently encounter in production apps. These examples demonstrate when to use each animation approach and how to structure your code for maintainability.</p>
<p>In this section, you'll learn how to build a collapsing header that shrinks as users scroll (using worklets for scroll tracking), a bottom sheet that responds to drag gestures (using worklets for gesture control), and a swipe-to-delete interaction for list items (combining worklets for gesture detection with animations for the deletion effect).</p>
<h3 id="heading-collapsing-header">Collapsing Header</h3>
<p>A collapsing header is a navigation bar that starts tall and shrinks as you scroll down. This pattern is popular because it maximizes content space while keeping navigation accessible. You'll use worklets here because the animation needs to follow the scroll position in real-time.</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">CollapsibleHeader</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> scrollY = useSharedValue(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">const</span> HEADER_MAX = <span class="hljs-number">200</span>;
  <span class="hljs-keyword">const</span> HEADER_MIN = <span class="hljs-number">60</span>;

  <span class="hljs-keyword">const</span> scrollHandler = useAnimatedScrollHandler({
    <span class="hljs-attr">onScroll</span>: <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      scrollY.value = event.contentOffset.y;
    },
  });

  <span class="hljs-keyword">const</span> headerStyle = useAnimatedStyle(<span class="hljs-function">() =&gt;</span> ({
    <span class="hljs-attr">height</span>: interpolate(
      scrollY.value,
      [<span class="hljs-number">0</span>, HEADER_MAX - HEADER_MIN],
      [HEADER_MAX, HEADER_MIN],
      <span class="hljs-string">'clamp'</span>
    ),
  }));

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">flex:</span> <span class="hljs-attr">1</span> }}&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{[styles.header,</span> <span class="hljs-attr">headerStyle</span>]}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Text</span>&gt;</span>My App<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.View</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.ScrollView</span>
        <span class="hljs-attr">onScroll</span>=<span class="hljs-string">{scrollHandler}</span>
        <span class="hljs-attr">scrollEventThrottle</span>=<span class="hljs-string">{16}</span>
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">height:</span> <span class="hljs-attr">1000</span>, <span class="hljs-attr">padding:</span> <span class="hljs-attr">16</span> }}&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">Text</span>&gt;</span>Scroll to see header collapse<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.ScrollView</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span></span>
  );
}
</code></pre>
<p>This pattern tracks scroll position in <code>scrollY</code> and uses <code>interpolate</code> to map it to header height. When you're at the top (scrollY = 0), the header is 200 pixels tall. As you scroll down 140 pixels, the header shrinks to 60 pixels. The animation happens on every frame as you scroll, which is why worklets are necessary – CSS animations couldn't track scroll position this smoothly.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762326945758/044ca9d6-dd6e-4891-b9f4-3a4dc8590b58.gif" alt="044ca9d6-dd6e-4891-b9f4-3a4dc8590b58" class="image--center mx-auto" width="295" height="640" loading="lazy"></p>
<h3 id="heading-bottom-sheet">Bottom Sheet</h3>
<p>A bottom sheet is a panel that slides up from the bottom of the screen, commonly used for action menus, filters, or additional content. Users can drag it to different heights or dismiss it with a swipe down. This requires worklets because it needs frame-by-frame gesture tracking.</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">BottomSheet</span>(<span class="hljs-params">{ children }</span>) </span>{
  <span class="hljs-keyword">const</span> translateY = useSharedValue(<span class="hljs-number">300</span>);
  <span class="hljs-keyword">const</span> context = useSharedValue({ <span class="hljs-attr">y</span>: <span class="hljs-number">0</span> });

  <span class="hljs-keyword">const</span> pan = Gesture.Pan()
    .onStart(<span class="hljs-function">() =&gt;</span> {
      context.value = { <span class="hljs-attr">y</span>: translateY.value };
    })
    .onChange(<span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      translateY.value = <span class="hljs-built_in">Math</span>.max(
        event.translationY + context.value.y,
        <span class="hljs-number">-300</span>
      );
    })
    .onEnd(<span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (event.velocityY &gt; <span class="hljs-number">500</span>) {
        translateY.value = withSpring(<span class="hljs-number">300</span>); <span class="hljs-comment">// Dismiss</span>
      } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (translateY.value &gt; <span class="hljs-number">-100</span>) {
        translateY.value = withSpring(<span class="hljs-number">-50</span>); <span class="hljs-comment">// Collapsed</span>
      } <span class="hljs-keyword">else</span> {
        translateY.value = withSpring(<span class="hljs-number">-300</span>); <span class="hljs-comment">// Expanded</span>
      }
    });

  <span class="hljs-keyword">const</span> animatedStyle = useAnimatedStyle(<span class="hljs-function">() =&gt;</span> ({
    <span class="hljs-attr">transform</span>: [{ <span class="hljs-attr">translateY</span>: translateY.value }],
  }));

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">GestureDetector</span> <span class="hljs-attr">gesture</span>=<span class="hljs-string">{pan}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{[styles.bottomSheet,</span> <span class="hljs-attr">animatedStyle</span>]}&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{styles.handle}</span> /&gt;</span>
        {children}
      <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.View</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">GestureDetector</span>&gt;</span></span>
  );
}
</code></pre>
<p>The bottom sheet starts off-screen at translateY = 300. When you start dragging, <code>.onStart()</code> saves the starting position in <code>context</code>. As you drag, <code>.onChange()</code> updates the position, but <code>Math.max()</code> prevents it from going below -300 (fully expanded). When you release, <code>.onEnd()</code> checks the velocity – if you swiped down quickly (velocity &gt; 500), it dismisses. Otherwise, it snaps to either the collapsed (-50) or expanded (-300) position based on where you released it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763051520805/dd09d10f-4d77-40a9-be9c-ef85d69be69e.gif" alt="Bottom sheet demo" class="image--center mx-auto" width="295" height="640" loading="lazy"></p>
<h3 id="heading-swipe-to-delete">Swipe to Delete</h3>
<p>Swipe-to-delete lets users remove items from a list by swiping left. It's a common pattern in email apps and to-do lists. This uses worklets for gesture tracking and timing functions for the deletion animation.</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">SwipeToDelete</span>(<span class="hljs-params">{ children, onDelete }</span>) </span>{
  <span class="hljs-keyword">const</span> translateX = useSharedValue(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">const</span> itemHeight = useSharedValue(<span class="hljs-number">60</span>);

  <span class="hljs-keyword">const</span> pan = Gesture.Pan()
    .activeOffsetX([<span class="hljs-number">-10</span>, <span class="hljs-number">10</span>])
    .onChange(<span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (event.translationX &lt; <span class="hljs-number">0</span>) {
        translateX.value = event.translationX;
      }
    })
    .onEnd(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-keyword">if</span> (translateX.value &lt; <span class="hljs-number">-100</span>) {
        translateX.value = withTiming(<span class="hljs-number">-500</span>, { <span class="hljs-attr">duration</span>: <span class="hljs-number">200</span> });
        itemHeight.value = withTiming(<span class="hljs-number">0</span>, { <span class="hljs-attr">duration</span>: <span class="hljs-number">200</span> }, <span class="hljs-function">() =&gt;</span> {
          runOnJS(onDelete)();
        });
      } <span class="hljs-keyword">else</span> {
        translateX.value = withSpring(<span class="hljs-number">0</span>);
      }
    });

  <span class="hljs-keyword">const</span> animatedStyle = useAnimatedStyle(<span class="hljs-function">() =&gt;</span> ({
    <span class="hljs-attr">transform</span>: [{ <span class="hljs-attr">translateX</span>: translateX.value }],
    <span class="hljs-attr">height</span>: itemHeight.value,
  }));

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">GestureDetector</span> <span class="hljs-attr">gesture</span>=<span class="hljs-string">{pan}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{[styles.item,</span> <span class="hljs-attr">animatedStyle</span>]}&gt;</span>
        {children}
      <span class="hljs-tag">&lt;/<span class="hljs-name">Animated.View</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">GestureDetector</span>&gt;</span></span>
  );
}
</code></pre>
<p>The <code>.activeOffsetX([-10, 10])</code> setting means the gesture only activates after you've moved 10 pixels horizontally, preventing accidental triggers during vertical scrolling. The <code>if (event.translationX &lt; 0)</code> check ensures you can only swipe left, not right. If you swipe past -100 pixels and release, it triggers the deletion: the item slides off-screen (-500), the height collapses to 0, and <code>runOnJS</code> calls your delete function from the UI thread back to JavaScript.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763051815614/6e2c2d60-3d2e-49ec-9fb5-c17db00e9120.gif" alt="6e2c2d60-3d2e-49ec-9fb5-c17db00e9120" class="image--center mx-auto" width="295" height="640" loading="lazy"></p>
<p>These patterns demonstrate the power of combining Reanimated's animation approaches with gesture handling. Now, let's look at how to keep these animations performing smoothly.</p>
<h2 id="heading-performance-optimizations">Performance Optimizations</h2>
<p>Even though Reanimated runs on the UI thread, poorly structured animations can still drop frames. Here are four key optimizations that will keep your animations consistently smooth at 60 FPS.</p>
<h3 id="heading-memoize-animations">Memoize Animations</h3>
<p>Every time your component re-renders, any animations you create inside the render function are recreated. This wastes memory and processing time.</p>
<p>Don't do this – creating a new animation object on every render:</p>
<pre><code class="lang-javascript">{items.map(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> (
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">entering</span>=<span class="hljs-string">{FadeIn.duration(300)}</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{item.id}</span> /&gt;</span></span>
))}
</code></pre>
<p>Instead, create the animation once outside the component or memoize it:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> fadeIn = FadeIn.duration(<span class="hljs-number">300</span>);
{items.map(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> (
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Animated.View</span> <span class="hljs-attr">entering</span>=<span class="hljs-string">{fadeIn}</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{item.id}</span> /&gt;</span></span>
))}
</code></pre>
<p>By storing the animation in a constant, you create it once and reuse the same object for all items. This reduces memory allocation and garbage collection, keeping your animations smooth even with long lists.</p>
<h3 id="heading-use-usederivedvalue">Use useDerivedValue</h3>
<p>If you're doing expensive calculations inside <code>useAnimatedStyle</code>, those calculations run every frame, even if the dependencies haven't changed.</p>
<p>Don't do this – recalculating every frame:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> animatedStyle = useAnimatedStyle(<span class="hljs-function">() =&gt;</span> ({
  <span class="hljs-attr">width</span>: <span class="hljs-built_in">Math</span>.min(<span class="hljs-built_in">Math</span>.max(offset.value * <span class="hljs-number">2</span>, <span class="hljs-number">100</span>), <span class="hljs-number">500</span>),
}));
</code></pre>
<p>Instead, use <code>useDerivedValue</code> to compute the value only when dependencies change:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> width = useDerivedValue(<span class="hljs-function">() =&gt;</span> 
  <span class="hljs-built_in">Math</span>.min(<span class="hljs-built_in">Math</span>.max(offset.value * <span class="hljs-number">2</span>, <span class="hljs-number">100</span>), <span class="hljs-number">500</span>)
);

<span class="hljs-keyword">const</span> animatedStyle = useAnimatedStyle(<span class="hljs-function">() =&gt;</span> ({
  <span class="hljs-attr">width</span>: width.value,
}));
</code></pre>
<p>Now the complex calculation only runs when <code>offset.value</code> changes, not on every frame. The <code>useAnimatedStyle</code> just reads the pre-computed width, which is much faster.</p>
<h3 id="heading-batch-updates">Batch Updates</h3>
<p>When you update multiple shared values, each update can trigger a separate re-render. This creates unnecessary work for the UI thread.</p>
<p>Don't do this – triggering multiple re-renders:</p>
<pre><code class="lang-javascript">scale.value = withSpring(<span class="hljs-number">1.2</span>);
opacity.value = withSpring(<span class="hljs-number">0.8</span>);
</code></pre>
<p>Instead, batch the updates using <code>runOnUI</code>:</p>
<pre><code class="lang-javascript">runOnUI(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-string">'worklet'</span>;
  scale.value = withSpring(<span class="hljs-number">1.2</span>);
  opacity.value = withSpring(<span class="hljs-number">0.8</span>);
})();
</code></pre>
<p>The <code>runOnUI</code> function ensures both updates happen in the same frame, so the UI only re-renders once. This is especially important when updating many values at once, like in complex gestures or choreographed animations.</p>
<h3 id="heading-prefer-transform-over-layout">Prefer Transform Over Layout</h3>
<p>Animating layout properties like width, height, or margins forces React Native to recalculate the position of every element that depends on the changing element. This is expensive.</p>
<p>Don't do this – expensive layout recalculation:</p>
<pre><code class="lang-javascript">width: withSpring(newWidth)
</code></pre>
<p>Instead, use transform properties, which only affect the visual appearance without triggering layout:</p>
<pre><code class="lang-javascript">transform: [{ <span class="hljs-attr">scaleX</span>: withSpring(scale) }]
</code></pre>
<p>Transform operations are hardware-accelerated and don't affect layout, making them dramatically faster. Whenever possible, use <code>translateX/Y</code> instead of changing position, <code>scale</code> instead of changing size, and <code>rotate</code> instead of changing orientation.</p>
<p>These optimizations will keep your animations buttery smooth. Now let's look at how to debug issues when they arise.</p>
<h2 id="heading-debugging-tips">Debugging Tips</h2>
<p>Even with proper setup, you may encounter issues with animations. Here are the most common problems and their solutions, written as complete troubleshooting steps.</p>
<h3 id="heading-animations-not-working">Animations Not Working</h3>
<p>If your animations aren't running at all, the most common cause is a missing or incorrectly configured Babel plugin. Open your <code>babel.config.js</code> file and verify that <code>react-native-worklets/plugin</code> is present in the plugins array and is the last plugin in the list. The order matters because the worklets plugin needs to process your code after all other transformations.</p>
<p>After confirming the plugin is correctly configured, clear your Metro bundler cache by running <code>npm start -- --reset-cache</code>, then rebuild your app completely. Simply reloading JavaScript won't work because Babel transformations happen during the build process.</p>
<h3 id="heading-app-crashes-on-startup-or-reload">App Crashes on Startup or Reload</h3>
<p>If your app crashes immediately after installing Reanimated or when you reload, the native modules likely aren't properly linked. With React Native 0.76+, this usually means the pods weren't installed or the native build is out of sync.</p>
<p>For iOS, run <code>cd ios &amp;&amp; pod install &amp;&amp; cd ..</code> then do a clean build with <code>npx react-native run-ios</code>. For Android, clean the build with <code>cd android &amp;&amp; ./gradlew clean &amp;&amp; cd ..</code> then rebuild with <code>npx react-native run-android</code>.</p>
<p>If you're getting build errors about missing headers or modules, make sure you've added both <code>react-native-reanimated</code> and <code>react-native-worklets</code> to your package.json dependencies.</p>
<h3 id="heading-turbomoduleregistry-not-found">"TurboModuleRegistry Not Found"</h3>
<p>If you see an error message saying "TurboModuleRegistry.get('NativeReanimated'): 'NativeReanimated' could not be found", it means the native code hasn't been properly linked to your JavaScript code.</p>
<p>First, verify you're using React Native 0.76 or newer, as Reanimated 4 requires the New Architecture. Check your <code>ios/Podfile</code> for <code>ENV['RCT_NEW_ARCH_ENABLED'] = '1'</code> and <code>android/gradle.properties</code> for <code>newArchEnabled=true</code>.</p>
<p>Then rebuild completely: <code>cd ios &amp;&amp; pod install &amp;&amp; cd .. &amp;&amp; npx react-native run-ios</code>.</p>
<h3 id="heading-logging-and-inspecting-shared-values">Logging and Inspecting Shared Values</h3>
<p>If you try to debug worklets using <code>console.log()</code>, you'll notice nothing appears in your console. This is because worklets run on the UI thread, which doesn't have direct access to the JavaScript console.</p>
<p>To log values from worklets, use the <code>useDerivedValue</code> hook:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> offset = useSharedValue(<span class="hljs-number">0</span>);

useDerivedValue(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Offset:'</span>, offset.value);
  <span class="hljs-keyword">return</span> offset.value;
});
</code></pre>
<p>For more advanced debugging, React Native's built-in debugger (accessed through dev menu → "Open Debugger") now supports debugging both threads. You can set breakpoints in worklets and inspect shared values in real-time.</p>
<h3 id="heading-monitor-performance">Monitor Performance</h3>
<p>To see if your animations are actually running at 60 FPS, enable the Performance Monitor built into React Native. Shake your device (or press Cmd+D in the iOS simulator, Cmd+M in Android emulator) to open the dev menu, then select "Show Perf Monitor".</p>
<p>The monitor displays two critical numbers: JS thread FPS and UI thread FPS. Your animations run on the UI thread, so watch that number. If it stays at 60 FPS, your animations are smooth. If it drops below 60, your animations are skipping frames and will appear janky. The JS thread FPS shows whether your React code is keeping up – if this drops, it indicates issues with your component renders, not your animations.</p>
<p>For more detailed debugging information and advanced troubleshooting, check the <a target="_blank" href="https://docs.swmansion.com/react-native-reanimated/docs/guides/debugging/">official debugging guide here</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763053624344/7aeffb5f-4829-4871-bd49-6e589adeb8ad.png" alt="7aeffb5f-4829-4871-bd49-6e589adeb8ad" class="image--center mx-auto" width="1176" height="1090" loading="lazy"></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Reanimated 4 gives you two powerful approaches to animation: CSS animations for simple state changes and worklets for complex, interactive animations that need real-time control.</p>
<p>Start with CSS transitions when building your next animation feature. They're simpler to write, easier to maintain, and perfect for the majority of UI animations. Reach for worklets when you need gesture control, scroll effects, or any animation that requires frame-by-frame updates.</p>
<p>The <a target="_blank" href="https://docs.swmansion.com/react-native-reanimated">official documentation</a> provides complete API references, detailed guides, and interactive examples. The <a target="_blank" href="https://github.com/software-mansion/react-native-reanimated">GitHub repository</a> includes production-ready sample code you can study and adapt.</p>
<p>Building smooth animations isn't just about technical capability – it's about creating experiences that feel responsive, intuitive, and delightful to use. Reanimated 4 makes achieving that standard straightforward, whether you're animating a simple button press or building a complex screen transition with multiple coordinated elements.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Mobile App Development with React Native ]]>
                </title>
                <description>
                    <![CDATA[ Mobile app development has become an essential skill in today’s digital landscape, and React Native is one of the leading tools for creating powerful, cross-platform mobile apps. React Native combines the best parts of React and native app developmen... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/mobile-app-development-with-react-native/</link>
                <guid isPermaLink="false">6772ae6d57dd031ab1ad4671</guid>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Mon, 30 Dec 2024 14:30:05 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735568898507/db93197c-fbed-454d-8134-49b398c4a5df.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Mobile app development has become an essential skill in today’s digital landscape, and React Native is one of the leading tools for creating powerful, cross-platform mobile apps. React Native combines the best parts of React and native app development, enabling developers to write code once and deploy it to both iOS and Android platforms. If you've ever wanted to dive into mobile development but felt overwhelmed by where to start, this comprehensive tutorial is perfect for you.</p>
<p>We just published a course on the <a target="_blank" href="http://freeCodeCamp.org">freeCodeCamp.org</a> YouTube channel that will teach you all about React Native. This beginner-friendly course, created by Dave Gray, is packed with over four hours of clear, hands-on instruction. Whether you're completely new to app development or have some experience with React, this tutorial will guide you through the essentials of React Native, helping you build functional, visually appealing, and dynamic mobile apps.</p>
<h3 id="heading-what-youll-learn">What You’ll Learn</h3>
<p>The course is structured into eight chapters, each focusing on a critical aspect of React Native development. Here’s a breakdown of what you can expect:</p>
<ol>
<li><p><strong>Intro</strong><br> A quick introduction to React Native, what it is, and why it’s such a popular choice for mobile development. You’ll set up your environment and get ready to code.</p>
</li>
<li><p><strong>Chapter 1: Start Here</strong><br> This chapter introduces the basics of React Native, including setting up your development environment and understanding the fundamental building blocks of a React Native app.</p>
</li>
<li><p><strong>Chapter 2: Build an App</strong><br> Dive into building your first simple app. You’ll learn how to create components, style them, and structure your app for better readability and functionality.</p>
</li>
<li><p><strong>Chapter 3: Navigation</strong><br> Learn how to implement navigation between different screens using React Navigation. This is essential for creating multi-screen apps that feel seamless to users.</p>
</li>
<li><p><strong>Chapter 4: List Views</strong><br> Explore how to display and manage lists in React Native, an essential skill for creating dynamic and interactive user interfaces.</p>
</li>
<li><p><strong>Chapter 5: CRUD App</strong><br> Build a simple CRUD (Create, Read, Update, Delete) application. This chapter introduces you to state management and handling user input, two critical concepts in app development.</p>
</li>
<li><p><strong>Chapter 6: Data Storage</strong><br> Learn how to store and retrieve data locally, which is crucial for offline functionality and saving user preferences.</p>
</li>
<li><p><strong>Chapter 7: Dynamic Routing</strong><br> Expand your knowledge of routing by implementing dynamic routes that adapt to the user’s input or app state.</p>
</li>
<li><p><strong>Chapter 8: EAS Development Builds</strong><br> Get introduced to Expo Application Services (EAS), a powerful tool for building and deploying your React Native apps.</p>
</li>
</ol>
<h3 id="heading-why-learn-react-native">Why Learn React Native?</h3>
<p>React Native is a popular framework because it allows you to use JavaScript—a widely used programming language—to build mobile apps. This means you can transition from web development to mobile development without needing to learn a completely new language. Plus, React Native’s community and ecosystem are rich with resources, making it easier for beginners to find support and tools.</p>
<h3 id="heading-start-building-today">Start Building Today</h3>
<p>This course provides everything you need to get started with React Native, from understanding the basics to deploying your app. By the end, you’ll have built multiple projects, gaining the confidence and skills to tackle your own mobile app ideas.</p>
<p>Ready to start your mobile app development journey? Check out the full course on the <a target="_blank" href="https://www.youtube.com/watch?v=sm5Y7Vtuihg">freeCodeCamp.org YouTube channel</a> (4-hour watch).</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/sm5Y7Vtuihg" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Mobile App Development Course with React Native, Supabase, Next.js ]]>
                </title>
                <description>
                    <![CDATA[ Mobile app development has evolved tremendously, and creating a robust, full-featured app today involves mastering both the front-end and back-end. If you're looking to build something practical, like an e-commerce platform, and want to learn the ins... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/mobile-app-development-course-with-react-native-supabase-nextjs/</link>
                <guid isPermaLink="false">670fce62ee042ff16bdfd24f</guid>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Wed, 16 Oct 2024 14:32:02 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729089105157/93e64814-1222-433c-839d-69f16fc3220b.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Mobile app development has evolved tremendously, and creating a robust, full-featured app today involves mastering both the front-end and back-end. If you're looking to build something practical, like an e-commerce platform, and want to learn the ins and outs of native mobile app development, this course has you covered. You'll be guided through the process of building a complete gadgets-selling platform, starting from the front-end design with React Native to setting up a secure back-end with Supabase, handling payments with Stripe, and even deploying the app with Next.js for the admin panel.</p>
<p>We just published a course on the <a target="_blank" href="http://freeCodeCamp.org">freeCodeCamp.org</a> YouTube channel that will teach you all about developing native mobile apps using React Native, Supabase, Stripe, and Next.js. This course is designed to take you through the entire process of building a complete e-commerce platform, making it an ideal project for anyone looking to strengthen their full-stack development skills. Whether you're new to mobile development or looking to expand your knowledge, this course covers everything step by step. Alaribe Bright developed this course.</p>
<h3 id="heading-what-youll-learn">What You’ll Learn</h3>
<ol>
<li><p><strong>React Native &amp; Expo</strong>: You'll start by creating a React Native app using Expo, a powerful framework that simplifies mobile app development. You'll dive into Expo Router for managing navigation with file-based routing, build dynamic pages for product categories, and manage cart functionality using Zustand, a lightweight state management library.</p>
</li>
<li><p><strong>Supabase</strong>: This section introduces Supabase, an open-source alternative to Firebase. You'll learn to handle authentication, database management, and real-time updates. You'll implement secure user authentication, explore SQL functions, triggers, and set up row-level security for a secure back-end.</p>
</li>
<li><p><strong>Next.js for Admin Panel</strong>: The admin panel is built using Next.js, known for its server-side rendering and API capabilities. You'll learn to manage categories and products, handle role-based access, and protect routes. The panel will allow you to perform CRUD operations (Create, Read, Update, Delete) efficiently on your platform’s data.</p>
</li>
<li><p><strong>Stripe for Payments</strong>: This course will also guide you through integrating Stripe to handle payments securely. You’ll learn how to create Stripe customers, set up checkout sessions, and manage payments, all while keeping customer data safe.</p>
</li>
<li><p><strong>Push Notifications</strong>: To enhance the user experience, you'll add real-time push notifications using EAS (Expo Application Services), ensuring your app can notify users of updates like order status changes in real time.</p>
</li>
<li><p><strong>Deployment</strong>: Finally, you'll deploy your project to Vercel, ensuring your admin panel and the entire application are live and accessible. You'll also troubleshoot deployment fixes and ensure your app runs smoothly in production.</p>
</li>
</ol>
<h3 id="heading-conclusion">Conclusion</h3>
<p>By the end of this course, you'll have a fully functioning e-commerce mobile app with an admin panel, payment integration, real-time updates, and secure authentication. This project is an excellent addition to your portfolio, demonstrating your ability to handle complex, full-stack development.</p>
<p>Watch the full course on <a target="_blank" href="https://youtu.be/2esQdKzRUCw">the freeCodeCamp.org YouTube channel</a> (12-hour watch).</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/2esQdKzRUCw" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Create a MacOS App with React Native ]]>
                </title>
                <description>
                    <![CDATA[ We are excited to announce the release of our latest course on the freeCodeCamp.org YouTube channel that will teach you how to use React Native to build a MacOS application. This course will guide you through the development of a fully integrated Mac... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/create-a-macos-app-with-react-native/</link>
                <guid isPermaLink="false">66c791dfba1f8664dd9a94e8</guid>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Thu, 22 Aug 2024 19:30:39 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724355029103/aba230f3-c814-43f5-afc0-4682355a1ddd.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>We are excited to announce the release of our latest course on the <a target="_blank" href="http://freeCodeCamp.org">freeCodeCamp.org</a> YouTube channel that will teach you how to use React Native to build a MacOS application. This course will guide you through the development of a fully integrated MacOS application using React Native.</p>
<p>You'll learn how to create an app that allows users to search and explore books using the Google Books API, manage personal bookshelves, and even generate AI-powered summaries. By the end of this course, you'll have a comprehensive understanding of building advanced MacOS applications.</p>
<h3 id="heading-course-overview"><strong>Course Overview</strong></h3>
<p>This course, created by Brijen Makwana, provides a step-by-step guide to building a sophisticated MacOS app using React Native. You'll gain hands-on experience with essential tools and techniques, from setting up your development environment to implementing advanced features like AI-powered book summaries and state management.</p>
<p>Here are the key things you will learn:</p>
<ul>
<li><p><strong>Setup and Initialization</strong>: Begin by setting up React Native on your Mac and initializing your project. You'll learn how to clean up the project structure to ensure a smooth development process.</p>
</li>
<li><p><strong>Building Components</strong>: Develop key components such as the SearchBar and BookItem, which are crucial for user interaction and displaying book information.</p>
</li>
<li><p><strong>Integrating Google Books API</strong>: Discover how to use Postman to test the Google Books API and integrate it into your project, enabling users to search and explore a vast library of books.</p>
</li>
<li><p><strong>State Management with Tanstack Query and Zustand</strong>: Learn how to manage application state efficiently using Tanstack Query and Zustand. You'll create custom hooks like useSearch and useBook to streamline data fetching and state updates.</p>
</li>
<li><p><strong>Navigation and Routing</strong>: Implement React Navigation to ensure smooth transitions between different screens in your app. You'll add routes for the Book Screen and Bookshelves Screen, enhancing the user experience.</p>
</li>
<li><p><strong>AI-Powered Features</strong>: Explore the integration of AI by setting up Google Generative AI and creating a custom hook, useAI, to generate book summaries. This feature adds a modern touch to your app, providing users with concise and insightful book overviews.</p>
</li>
<li><p><strong>Local Storage and Book Management</strong>: Implement local storage support to save user data and manage bookshelves effectively. You'll learn how to update and remove books using Zustand, ensuring a seamless user experience.</p>
</li>
</ul>
<p>The course is structured into detailed sections, each focusing on a specific aspect of app development:</p>
<ul>
<li><p>Book Management MacOS App (React Native)</p>
</li>
<li><p>Setup React Native on your Mac</p>
</li>
<li><p>Initialize the Project</p>
</li>
<li><p>Cleanup the Project</p>
</li>
<li><p>SearchBar Component</p>
</li>
<li><p>Intro to Google Books API</p>
</li>
<li><p>Use Postman to test the API</p>
</li>
<li><p>Integrate Google Books API in the Project</p>
</li>
<li><p>Setup Tanstack Query</p>
</li>
<li><p>useSearch Custom Hook</p>
</li>
<li><p>BookItem Component</p>
</li>
<li><p>Render Books on Home Screen</p>
</li>
<li><p>Add support for Icons in the Project</p>
</li>
<li><p>Rating in BookItem</p>
</li>
<li><p>Setup React Navigation</p>
</li>
<li><p>Add new Route for Book Screen</p>
</li>
<li><p>useBook Custom Hook</p>
</li>
<li><p>Implement Book Screen</p>
</li>
<li><p>Add types to React Navigation</p>
</li>
<li><p>SelectBookShelf Component</p>
</li>
<li><p>Add new Route for Bookshelves Screen</p>
</li>
<li><p>SegmentedButtons Component</p>
</li>
<li><p>Intro to Zustand</p>
</li>
<li><p>Setup Zustand</p>
</li>
<li><p>Zustand store for Bookshelves</p>
</li>
<li><p>BookShelfItem Component</p>
</li>
<li><p>Render Books on Bookshelves Screen</p>
</li>
<li><p>Implement update book in Zustand</p>
</li>
<li><p>Implement remove book in Zustand</p>
</li>
<li><p>Implement Local Storage Support</p>
</li>
<li><p>AI Book Summary</p>
</li>
<li><p>Setup Google Generative AI</p>
</li>
<li><p>useAI Custom Hook</p>
</li>
<li><p>Outro</p>
</li>
</ul>
<h3 id="heading-join-us-today"><strong>Join Us Today</strong></h3>
<p>Head over to the <a target="_blank" href="https://youtu.be/-kizZZrh1zM">freeCodeCamp.org YouTube channel</a> to watch the course (3-hour watch). Whether you're looking to enhance your current projects or embark on new ones, this course will help you master MacOS application development.</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/-kizZZrh1zM" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Build a Meditation App with React Native & Expo Router ]]>
                </title>
                <description>
                    <![CDATA[ Are you looking to enhance your React Native skills while creating a practical and soothing application? This new course from freeCodeCamp.org is perfect for you! You'll learn how to build a meditation app using Expo, an open-source platform for maki... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-meditation-app-with-react-native-expo-router/</link>
                <guid isPermaLink="false">6685759dc4274328d1d57108</guid>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Wed, 03 Jul 2024 16:00:29 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1720022414585/f0f87e6e-c5b4-4ea3-8ccb-d56c14e2d94f.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Are you looking to enhance your React Native skills while creating a practical and soothing application? This new course from <a target="_blank" href="http://freeCodeCamp.org">freeCodeCamp.org</a> is perfect for you! You'll learn how to build a meditation app using Expo, an open-source platform for making universal native apps for Android, iOS, and the web with JavaScript and React. Throughout the course, you'll leverage TypeScript, React, NativeWind, and other powerful tools to develop a simple yet functional mobile app that promotes relaxation and mindfulness.</p>
<p>This comprehensive tutorial will guide you through every step of the development process, from setting up your local development environment to implementing various screens and functionalities within the app. You'll also learn how to incorporate TypeScript for type safety, use NativeWind for styling, and manage your app's navigation and state effectively.</p>
<p>Here's a glimpse of what you'll learn in this course:</p>
<h3 id="heading-introduction">Introduction</h3>
<p>You will start by understanding the fundamentals of creating a meditation app using Expo and React Native, providing a solid foundation for the rest of the project.</p>
<h3 id="heading-setup-local-development-environment">Setup Local Development Environment</h3>
<p>Learn how to set up your local development environment, including installing necessary tools and dependencies to get started with Expo and React Native.</p>
<h3 id="heading-add-static-assets-and-define-constant-files">Add Static Assets and Define Constant Files</h3>
<p>Understand how to add static assets such as images and icons to your project and define constant files for better organization and maintainability.</p>
<h3 id="heading-setup-nativewind">Setup NativeWind</h3>
<p>Explore how to set up NativeWind for styling your application components in a consistent and efficient manner.</p>
<h3 id="heading-implement-initial-screen">Implement Initial Screen</h3>
<p>Implement the initial screen of your meditation app, laying the groundwork for a smooth user experience.</p>
<h3 id="heading-splash-screen-image">Splash Screen Image</h3>
<p>Create an engaging splash screen image to enhance the visual appeal of your app during the startup phase.</p>
<h3 id="heading-the-userouter-hook">The useRouter Hook</h3>
<p>Learn to use the useRouter hook for handling navigation within your app, ensuring seamless transitions between different screens.</p>
<h3 id="heading-add-appgradient-component-and-tabs-directory">Add AppGradient Component and Tabs Directory</h3>
<p>Enhance your app's UI by adding a gradient component and organizing your tabs directory for better structure and navigation.</p>
<h3 id="heading-the-meditation-index-screen">The Meditation Index Screen</h3>
<p>Build the Meditation Index Screen, where users can browse through available meditation sessions.</p>
<h3 id="heading-use-flatlist-on-the-meditation-index-screen">Use FlatList on the Meditation Index Screen</h3>
<p>Implement FlatList to efficiently display a list of meditation sessions, ensuring a smooth scrolling experience.</p>
<h3 id="heading-the-affirmations-index-screen">The Affirmations Index Screen</h3>
<p>Create an Affirmations Index Screen, allowing users to explore various positive affirmations.</p>
<h3 id="heading-the-affirmations-detail-screen">The Affirmations Detail Screen</h3>
<p>Develop the Affirmations Detail Screen to provide more information and details about each affirmation.</p>
<h3 id="heading-the-meditation-detail-screen">The Meditation Detail Screen</h3>
<p>Implement the Meditation Detail Screen, giving users access to specific meditation session details and options.</p>
<h3 id="heading-meditation-audiosong">Meditation Audio/Song</h3>
<p>Incorporate meditation audio or songs into your app, enhancing the overall meditation experience for users.</p>
<h3 id="heading-react-context-and-modal-screen">React Context and Modal Screen</h3>
<p>Learn how to use React Context for state management and create a modal screen for additional interactive elements within your app.</p>
<p>By the end of this course, you will have built a fully functional meditation app while gaining valuable experience with Expo, React Native, TypeScript, and other essential tools. Whether you're a beginner or an experienced developer, this course offers something for everyone, helping you grow your skills and create impactful mobile applications.</p>
<p>Watch the full course on <a target="_blank" href="https://youtu.be/9UKCv9T_rIo">the freeCodeCamp.org YouTube channel</a> (2-hour watch).</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/9UKCv9T_rIo" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Store Data Locally in React Native Expo ]]>
                </title>
                <description>
                    <![CDATA[ React Native has grown in popularity as a mobile application development tool because of its ability to create cross-platform applications using familiar JavaScript and React principles.  When building mobile applications, one common requirement is t... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-store-data-locally-in-react-native-expo/</link>
                <guid isPermaLink="false">66b9ee747bae781916c2d6dd</guid>
                
                    <category>
                        <![CDATA[ localstorage ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ John Caleb ]]>
                </dc:creator>
                <pubDate>Mon, 13 May 2024 11:42:45 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/05/local-storage-in--react-native-expo--1-.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>React Native has grown in popularity as a mobile application development tool because of its ability to create cross-platform applications using familiar JavaScript and React principles. </p>
<p>When building mobile applications, one common requirement is the ability to save data locally on the device. This is when local storage comes into play. <a target="_blank" href="https://docs.expo.dev/versions/latest/sdk/async-storage/">Async Storage</a>, provided by React Native Expo, is a simple but powerful solution for saving data locally within your React Native Expo apps.</p>
<p>In this tutorial, we'll discuss the fundamentals of local storage, introduce Async Storage, and demonstrate how to properly integrate it into React Native Expo projects.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></li>
<li><a class="post-section-overview" href="#heading-what-is-local-storage">What is Local Storage?</a></li>
<li><a class="post-section-overview" href="#heading-what-is-async-storage">What is Async Storage?</a></li>
<li><a class="post-section-overview" href="#heading-how-to-get-started-with-async-storage">How to Get Started with Async Storage</a></li>
<li><a class="post-section-overview" href="#heading-understanding-async-storage-methods">Understanding Async Storage Methods</a></li>
<li><a class="post-section-overview" href="#heading-advanced-usage-and-best-practices">Advanced Usage and Best Practices</a></li>
<li><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li>Familiarity with React Native and JavaScript.</li>
<li>Node.js and npm (or yarn) installed.</li>
</ul>
<h2 id="heading-what-is-local-storage">What is Local Storage?</h2>
<p>Local storage is an essential component of mobile app development, allowing developers to store data on the user's device. Unlike other storage choices such as databases, Local Storage uses a straightforward key-value pair storage technique.</p>
<p>Developers can use it to save small quantities of data that remain stored even when they close the program or restart the device. This makes it excellent for storing user preferences, authentication tokens, and other important information.</p>
<p>Local storage is essential for boosting app speed because it eliminates the need to retrieve data from faraway servers regularly.</p>
<h2 id="heading-what-is-async-storage">What is Async Storage?</h2>
<p>Async Storage is a key-value storage system supplied by React Native Expo that allows you to manage local storage in mobile apps. It provides a simple key-value storage system that enables developers to store and retrieve data asynchronously. </p>
<p>Unlike synchronous storage methods, Async Storage allows you to save and retrieve data without interrupting the main thread, resulting in a more seamless user experience.</p>
<h2 id="heading-how-to-get-started-with-async-storage">How to Get Started with Async Storage</h2>
<p>To use Async Storage in your React Native Expo project, ensure that Expo is installed. If you haven't already set up a React Native Expo project, you can do so by installing Expo CLI:</p>
<pre><code class="lang-bash">$ npm install -g expo-cli
</code></pre>
<p>Create a new Expo project:</p>
<pre><code class="lang-bash">$ expo init MyProject
$ <span class="hljs-built_in">cd</span> MyProject
</code></pre>
<p>To add Async Storage to your project, run the following command:</p>
<pre><code class="lang-js">$ expo install @react-native-<span class="hljs-keyword">async</span>-storage/<span class="hljs-keyword">async</span>-storage
</code></pre>
<p>The <code>@react-native-async-storage/async-storage</code> is a community-maintained version of AsyncStorage. Once installed, you can then setup a file to handle the AsyncStorage methods such as <code>setItem()</code>, <code>updateItem()</code>, <code>deleteItem()</code>, and others. This file would be imported whenever you want to make a call to the local storage. </p>
<p>In this example, we'll create a folder named <code>utils</code> in the root path of our project and then create the <code>AsyncStorage.js</code> file to handle these methods:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/05/-4611C08C-3557-460A-A7CC-BFD754BD13F7-.png.jpg" alt="Async Storage File Structure " width="600" height="400" loading="lazy">
<em>Async Storage File Structure</em></p>
<p>Within the <code>AsyncStorage.js</code> file, you can define the AsyncStorage methods like this:</p>
<pre><code class="lang-js"><span class="hljs-comment">// utils/AsyncStorage.js</span>

<span class="hljs-keyword">import</span> AsyncStorage <span class="hljs-keyword">from</span> <span class="hljs-string">'@react-native-async-storage/async-storage'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> setItem = <span class="hljs-keyword">async</span> (key, value) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> AsyncStorage.setItem(key, <span class="hljs-built_in">JSON</span>.stringify(value));
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error setting item:'</span>, error);
  }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getItem = <span class="hljs-keyword">async</span> (key) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> value = <span class="hljs-keyword">await</span> AsyncStorage.getItem(key);
    <span class="hljs-keyword">return</span> value != <span class="hljs-literal">null</span> ? <span class="hljs-built_in">JSON</span>.parse(value) : <span class="hljs-literal">null</span>;
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error getting item:'</span>, error);
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> removeItem = <span class="hljs-keyword">async</span> (key) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> AsyncStorage.removeItem(key);
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error removing item:'</span>, error);
  }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> mergeItem = <span class="hljs-keyword">async</span> (key, value) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> AsyncStorage.mergeItem(key, <span class="hljs-built_in">JSON</span>.stringify(value));
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error merging item:'</span>, error);
  }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> clear = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> AsyncStorage.clear();
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error clearing AsyncStorage:'</span>, error);
  }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getAllKeys = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> AsyncStorage.getAllKeys();
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error getting all keys:'</span>, error);
    <span class="hljs-keyword">return</span> [];
  }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getAllItems = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> keys = <span class="hljs-keyword">await</span> AsyncStorage.getAllKeys();
    <span class="hljs-keyword">const</span> items = <span class="hljs-keyword">await</span> AsyncStorage.multiGet(keys);
    <span class="hljs-keyword">return</span> items.reduce(<span class="hljs-function">(<span class="hljs-params">accumulator, [key, value]</span>) =&gt;</span> {
      accumulator[key] = <span class="hljs-built_in">JSON</span>.parse(value);
      <span class="hljs-keyword">return</span> accumulator;
    }, {});
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error getting all items:'</span>, error);
    <span class="hljs-keyword">return</span> {};
  }
};
</code></pre>
<p>Moving forward, In the next section, we'll explain and break down the meaning of these AsyncStorage functions in the AsyncStorage.js file.</p>
<h2 id="heading-understanding-async-storage-methods">Understanding Async Storage Methods</h2>
<p>By separating AsyncStorage functions into their own file, you can conveniently manage and reuse them throughout your React Native Expo project. This modular approach improves code maintenance and readability.</p>
<p>In the previous section, we created the <code>AsyncStorage.js</code> file and added several functions. </p>
<p>In the following sections, we'll talk about these methods and how to use them effectively.</p>
<h3 id="heading-setitem"><code>setItem()</code></h3>
<p>This method is essential for storing data locally on the device. It allows developers to store key-value pairs in AsyncStorage, where the key serves as a unique identifier and the value represents the data to be stored.</p>
<pre><code class="lang-js"><span class="hljs-keyword">await</span> AsyncStorage.setItem(<span class="hljs-string">'username'</span>, <span class="hljs-string">'freeCodeCamp'</span>);
</code></pre>
<h3 id="heading-getitem"><code>getItem()</code></h3>
<p>The <code>getItem()</code> method returns the value associated with a given key from local storage. It sends a parameter/key, which is the unique identification of the data being requested. And it returns the value associated with the supplied key, or null if no value is discovered for the given key.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> username = <span class="hljs-keyword">await</span> AsyncStorage.getItem(<span class="hljs-string">'username'</span>);
</code></pre>
<p>In this scenario, the value for the key <code>username</code> is fetched from local storage. This obtained value, <code>freeCodeCamp</code>, is then placed in the variable username, making it available for further use across the application.</p>
<h3 id="heading-removeitem"><code>removeItem()</code></h3>
<p>This function deletes the object with the supplied key from local storage. It's useful when you want to remove a specific piece of data that's no longer required.</p>
<pre><code class="lang-js"><span class="hljs-keyword">await</span> AsyncStorage.removeItem(<span class="hljs-string">'username'</span>);
</code></pre>
<p>In this example, the object identified by the key <code>username</code> is removed from local storage.</p>
<h3 id="heading-mergeitem"><code>mergeItem()</code></h3>
<p>The <code>mergeItem()</code> method combines the value of an existing key with the value supplied as input. If the key do not exists, it works similarly to <code>setItem()</code>, creating a new key-value pair.</p>
<pre><code class="lang-js"><span class="hljs-keyword">await</span> AsyncStorage.mergeItem(<span class="hljs-string">'user'</span>, <span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-attr">name</span>: <span class="hljs-string">'John'</span> }));
</code></pre>
<h3 id="heading-clear"><code>clear()</code></h3>
<p>The <code>clear()</code> method deletes all items from local storage. It's useful when you wish to delete all local data, such as when you log out of a user or reset the application state.</p>
<pre><code class="lang-js"><span class="hljs-keyword">await</span> AsyncStorage.clear();
</code></pre>
<h3 id="heading-getallkeys"><code>getAllKeys()</code></h3>
<p>The <code>getAllKeys()</code> function returns all keys kept in local storage. It's useful when you need to loop through all keys or conduct operations based on the keys in local storage.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> keys = <span class="hljs-keyword">await</span> AsyncStorage.getAllKeys();
</code></pre>
<h3 id="heading-multiget"><code>multiGet()</code></h3>
<p>The <code>multiGet()</code> function obtains several key-value pairs from local storage using an array of keys provided.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> AsyncStorage.multiGet([<span class="hljs-string">'username'</span>, <span class="hljs-string">'email'</span>]);
</code></pre>
<p>In this example, the values for the keys <code>username</code> and <code>email</code> are fetched from local storage.</p>
<h2 id="heading-advanced-usage-and-best-practices">Advanced Usage and Best Practices</h2>
<p>While AsyncStorage provides a straightforward interface for local storage, there are several best practices to consider:</p>
<ol>
<li><strong>Data Serialization</strong>: When storing complex data types such as objects or arrays, remember to serialize them into a string format using <code>JSON.stringify()</code> before storing and deserialize them using <code>JSON.parse()</code> when retrieving.</li>
<li><strong>Error Handling</strong>: Implement robust error handling to gracefully handle any failures that may occur during Async Storage operations.</li>
<li><strong>Security Considerations</strong>: Be mindful of the sensitivity of the data being stored locally and implement appropriate security measures such as encryption for sensitive information.</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In conclusion, using local storage into your React Native Expo projects is essential for developing robust and responsive mobile applications. Async Storage simplifies the process of storing and retrieving data on the device, providing a consistent user experience and enabling offline functionalities. </p>
<p>By following the steps provided in this article, you can utilize Async Storage to improve your apps local storage functionality.</p>
<p>Remember, if you have any questions or just want to say hi, feel free to reach me on <a target="_blank" href="https://twitter.com/thejohncaleb">X(Twitter)</a> or my <a target="_blank" href="https://thejohncaleb.netlify.app/contact">website</a>. :)  </p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Create a React Native Splash Screen ]]>
                </title>
                <description>
                    <![CDATA[ In this article, you'll get a hands-on practical guide for creating a native splash screen for React Native CLI applications.  Note that this tutorial is not applicable for apps created with Expo. SVG Icon Image and Background The first thing you nee... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/react-native-splash-screen/</link>
                <guid isPermaLink="false">66ba2d7cde9370f66eeb0a95</guid>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Lucas ]]>
                </dc:creator>
                <pubDate>Wed, 08 May 2024 19:17:54 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/05/article-1-rnsplash-2.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this article, you'll get a hands-on practical guide for creating a native splash screen for React Native CLI applications. </p>
<p>Note that this tutorial is not applicable for apps created with Expo.</p>
<h2 id="heading-svg-icon-image-and-background">SVG Icon Image and Background</h2>
<p>The first thing you need is an image. It can be in any format, but I recommend using SVG because, from it, you will generate icons of various sizes for different types of Android and iOS devices.</p>
<p>You will also need a background color that complements or contrasts your project's primary color. In my case, I will use #074C4E.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/05/nubble-logo.png" alt="Image" width="600" height="400" loading="lazy">
<em>icon imagem and backgorund color</em></p>
<h2 id="heading-add-the-image-to-the-project">Add the Image to the Project</h2>
<p>Next, add the SVG image to your project. It doesn't matter where. The most important thing is remembering the path because you will need to reference it later. </p>
<p>In my case, I placed it in <code>src/assets/svgs/logo-vertical-white.svg</code>.</p>
<h2 id="heading-how-to-use-the-react-native-bootsplash-library">How to Use the react-native-bootsplash Library</h2>
<p>You will use the <code>[react-native-bootsplash](https://github.com/zoontek/react-native-bootsplash)</code>  library to create native splash screens. This library will help you in three essential areas to guarantee your users an excellent experience when encountering the splash screen.</p>
<ol>
<li><strong>Native Splash Screens</strong>: React Native apps have a "JavaScript side" that only loads after the native side is ready. Therefore, to present a splash screen quickly, a native experience is necessary. The good news is that all the code is already inside the library, so you just need to connect it to your project.</li>
<li><strong>Generation of Images and Files</strong>: When creating native splash screens, it is necessary to create specific image files for each platform. This can be done through tools like Xcode and Android Studio. Fortunately, the library comes with a CLI (command-line interface) that allows you to generate these files with just one command!</li>
<li><strong>Hide at the Right Moment</strong>: In many cases, even after the native side has loaded, the app may still not be ready to display content to the user. On the JavaScript side, you still need to load your navigation stack, fetch the user's authentication status, or call the API to fetch some data. With the <code>react-native-bootsplash</code>, you can choose when to hide the splash screen.</li>
</ol>
<p>First, let's add the library. As I am using Yarn as my dependency manager, I will execute the command:</p>
<pre><code class="lang-bash">yarn add react-native-bootsplash
</code></pre>
<p>Since the library has native dependencies, you need to install the pods on the iOS side. Inside the <code>ios</code> folder, run the following command:</p>
<pre><code class="lang-bash">pod install
</code></pre>
<p>Great, the library installation is complete 😁. In case you're wondering, the native Android dependencies are automatically installed when you run the <code>yarn android</code> command. We'll do this later after finishing the setup.</p>
<h2 id="heading-how-to-generate-the-splash-screen-files">How to Generate the Splash Screen Files</h2>
<p>In addition to installing the library, you need to generate the files and images mentioned earlier and update a few native files after that.</p>
<p>The <code>react-native-bootsplash</code> has a command that helps us create all the necessary native files and images to create a native Android and iOS splash screen.</p>
<p>It's worth mentioning that the library also has a premium option, where you can buy a license key to unlock extra CLI commands, like adding more than one icon on the screen and generating different images for Dark Mode. You will use the simplest splash screen, so you don't need a license key. But I highly recommend it if you have any of the use cases mentioned above and also to support the library's author, who does an incredible job.</p>
<p>To generate the files, you'll need the following to run the command, which you should customize according to your project:</p>
<ol>
<li>File path and name: <code>src/assets/svgs/logo-vertica-white.svg</code></li>
<li>The background color: <code>074C4E</code></li>
<li>The logo width: <code>105</code></li>
</ol>
<pre><code class="lang-bash">yarn react-native generate-bootsplash src/assets/svgs/logo-vertica-white.svg \\
   --platforms=android,ios \\
   --background=074C4E \\
   --logo-width=105
</code></pre>
<p>After running this command, you will see that the native image files, color, and storyboard have been successfully generated.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/05/terminal.png" alt="Image" width="600" height="400" loading="lazy">
<em>terminal output</em></p>
<h2 id="heading-how-to-connect-the-library-to-the-project">How to Connect the Library to the Project</h2>
<p>It is time to integrate the library and newly created splash screen with the project by modifying some native files.</p>
<h3 id="heading-ios-appdelegatemm">iOS - AppDelegate.mm</h3>
<p>On iOS, the file where you configure libraries with native dependencies is the <strong>AppDelegate.mm</strong>. </p>
<p>And you will do this in two steps. First, import the library at the top of the file:</p>
<pre><code class="lang-cpp"><span class="hljs-meta">#import <span class="hljs-meta-string">"RNBootSplash.h"</span></span>
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/05/ios-import.png" alt="Image" width="600" height="400" loading="lazy">
<em>importing BootSplash on AppDelegate</em></p>
<p>The second change in this file is to add the function that will connect the native and JavaScript sides. Add this snippet at the end of the project before the last <code>@end</code>. The code will be different if you use a react-native version below 0.74.</p>
<pre><code class="lang-cpp"><span class="hljs-comment">// ⬇️ Add this before file @end (for react-native 0.74+)</span>
- (<span class="hljs-keyword">void</span>)customizeRootView:(RCTRootView *)rootView {
  [RNBootSplash initWithStoryboard:@<span class="hljs-string">"BootSplash"</span> rootView:rootView]; <span class="hljs-comment">// ⬅️ initialize the splash screen</span>
}

<span class="hljs-comment">// OR</span>

<span class="hljs-comment">// ⬇️ Add this before file @end (for react-native &lt; 0.74)</span>
- (UIView *)createRootViewWithBridge:(RCTBridge *)bridge
                          moduleName:(NSString *)moduleName
                           initProps:(NSDictionary *)initProps {
  UIView *rootView = [super createRootViewWithBridge:bridge moduleName:moduleName initProps:initProps];
  [RNBootSplash initWithStoryboard:@<span class="hljs-string">"BootSplash"</span> rootView:rootView]; <span class="hljs-comment">// ⬅️ initialize the splash screen</span>
  <span class="hljs-keyword">return</span> rootView;
}
</code></pre>
<p>In my case, I am on react-native 0.73, so my modification looks like this:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/05/ios-code.png" alt="Image" width="600" height="400" loading="lazy">
<em>added createRootViewWithBridge (react-native &lt; 0.74)</em></p>
<h3 id="heading-android-stylesxml">Android - styles.xml</h3>
<p>On Android, you need to change three native files. Let's start with <strong>styles.xml</strong>.</p>
<p>Inside the <strong>android/app/src/main/res/values/styles.xml</strong> file, add the following code snippet inside the <code>resources</code> tag. Remember, there is already a <code>style</code> tag within it – do not replace it. Add an extra one.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"BootTheme"</span> <span class="hljs-attr">parent</span>=<span class="hljs-string">"Theme.BootSplash"</span>&gt;</span><span class="xml">
    <span class="hljs-tag">&lt;<span class="hljs-name">item</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"bootSplashBackground"</span>&gt;</span>@color/bootsplash_background<span class="hljs-tag">&lt;/<span class="hljs-name">item</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">item</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"bootSplashLogo"</span>&gt;</span>@drawable/bootsplash_logo<span class="hljs-tag">&lt;/<span class="hljs-name">item</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">item</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"postBootSplashTheme"</span>&gt;</span>@style/AppTheme<span class="hljs-tag">&lt;/<span class="hljs-name">item</span>&gt;</span>
</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/05/android-styles.png" alt="Image" width="600" height="400" loading="lazy">
<em>styles.xml</em></p>
<h3 id="heading-android-androidmanifestxml">Android - AndroidManifest.xml</h3>
<p>To connect the splash screen in the file <strong>android/app/src/main/AndroidManifest.xml</strong>, you have to add the property <code>android:theme="@style/BootTheme"</code> inside the <code>activity</code>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/05/android-AndroidManifest.png" alt="Image" width="600" height="400" loading="lazy">
<em>My AndroidManifest.xml</em></p>
<h3 id="heading-android-modify-the-mainactivityjavakt">Android - Modify the MainActivity.java/kt</h3>
<p>You need to initiate the splash screen within the <code>MainActivity</code>. Depending on your version of React Native, your file may have a Java or Kotlin extension. You have to modify or create the <code>onCreate</code> method if it does not exist.</p>
<p>I literally copied the code below from the library <strong>README</strong> file, so you don't need to jump there but feel free to check it <a target="_blank" href="https://github.com/zoontek/react-native-bootsplash?tab=readme-ov-file#android-1">here</a>.</p>
<pre><code class="lang-java"><span class="hljs-comment">// Java (react-native &lt; 0.73)</span>
<span class="hljs-comment">// …</span>

<span class="hljs-comment">// add these required imports:</span>
<span class="hljs-keyword">import</span> android.os.Bundle;
<span class="hljs-keyword">import</span> com.zoontek.rnbootsplash.RNBootSplash;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainActivity</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ReactActivity</span> </span>{

  <span class="hljs-comment">// …</span>

  <span class="hljs-meta">@Override</span>
  <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCreate</span><span class="hljs-params">(Bundle savedInstanceState)</span> </span>{
    RNBootSplash.init(<span class="hljs-keyword">this</span>, R.style.BootTheme); <span class="hljs-comment">// ⬅️ initialize the splash screen</span>
    <span class="hljs-keyword">super</span>.onCreate(savedInstanceState); <span class="hljs-comment">// super.onCreate(null) with react-native-screens</span>
  }
}
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/05/android-MainActivity.png" alt="Image" width="600" height="400" loading="lazy">
<em>My final MainActivity.java</em></p>
<h3 id="heading-hide-the-splash-screen">Hide the Splash Screen</h3>
<p>The implementation is ready for both platforms! But before running the app, you must hide the splash screen at some point on the JavaScript side; otherwise, the app will open and get stuck.</p>
<p>Of course, where to put it depends significantly on what you need to load for your app to be ready to display to the user. A classic example is waiting for React Navigation to load the navigation stack, which is signaled through the <code>onReady</code> callback.</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> BootSplash <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native-bootsplash'</span>;
<span class="hljs-comment">// ...</span>

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Router</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// ...</span>
    <span class="hljs-keyword">return</span> (
    &lt;NavigationContainer onReady={<span class="hljs-function">() =&gt;</span> BootSplash.hide({fade: <span class="hljs-literal">true</span>})}&gt;
      {Stack}
    &lt;/NavigationContainer&gt;
  );
}
</code></pre>
<h3 id="heading-you-are-ready-to-go">You are ready to go!</h3>
<p>Your splash screen is ready for use! However, since you modified native files, rebuilding the app is necessary. To do this, run the commands <code>yarn ios</code> and <code>yarn android</code> to see how your implementation works.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/05/showcase.gif" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Thanks for reading! If you speak Portuguese and would like more content about React Native, subscribe to my YouTube channel <a target="_blank" href="https://www.youtube.com/@Coffstack">here</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Mobile Quiz App with React Native, ChatGPT and Supabase ]]>
                </title>
                <description>
                    <![CDATA[ In this tutorial, you'll learn how to build a mobile quiz application that authenticates users, allows them to take tests, and ranks them based on their scores.  The application leverages some of Supabase's features, such as authentication and databa... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-mobile-quiz-app/</link>
                <guid isPermaLink="false">66b8fc7a33470f39c663c1a4</guid>
                
                    <category>
                        <![CDATA[ chatgpt ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mobile app development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Asaolu ]]>
                </dc:creator>
                <pubDate>Thu, 29 Feb 2024 17:30:26 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/02/Building-a-mobile-quiz-app-with-React-Native--ChatGPT-and-Supabase--1--1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this tutorial, you'll learn how to build a mobile quiz application that authenticates users, allows them to take tests, and ranks them based on their scores. </p>
<p>The application leverages some of Supabase's features, such as authentication and database storage, to build a secured full-stack mobile application.</p>
<p>Additionally, you'll learn how to create React Native applications with Expo, generate a set of questions and answers from ChatGPT, and perform CRUD operations and user authentication with Supabase.</p>
<p>To fully understand this tutorial, you'll need to have a basic knowledge of React Native and data fetching in React applications.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><a class="post-section-overview" href="#heading-mobile-application-demo">Mobile Application Demo</a></li>
<li><a class="post-section-overview" href="#heading-how-to-set-up-a-react-native-application-with-expo-1">How to set up a React Native application with Expo</a></li>
<li><a class="post-section-overview" href="#heading-how-to-set-up-a-react-native-application-with-expo-1">How to style the React Native application with Tailwind CSS</a></li>
<li><a class="post-section-overview" href="#heading-how-to-build-the-application-screens">How to build the application screens</a></li>
<li><a class="post-section-overview" href="#heading-how-to-build-the-authentication-screens">How to build the authentication screens</a></li>
<li><a class="post-section-overview" href="#heading-how-to-build-the-tab-screens">How to build the tab screens</a></li>
<li><a class="post-section-overview" href="#heading-how-to-build-the-stack-screens">How to build the stack screens</a></li>
<li><a class="post-section-overview" href="#heading-how-to-generate-quiz-questions-and-answers-from-chatgpt">How to generate quiz questions and answers from ChatGPT</a></li>
<li><a class="post-section-overview" href="#heading-how-to-add-supabase-to-react-native">How to add Supabase to React Native</a></li>
<li><a class="post-section-overview" href="#heading-how-to-add-supabase-authentication-to-react-native-applications">How to add Supabase authentication to React Native applications</a></li>
<li><a class="post-section-overview" href="#heading-how-to-sign-up-new-users">How to sign up new users</a></li>
<li><a class="post-section-overview" href="#heading-how-to-sign-in-existing-users">How to sign in existing users</a></li>
<li><a class="post-section-overview" href="#heading-how-to-log-users-out-of-the-application">How to log users out of the application</a></li>
<li><a class="post-section-overview" href="#heading-how-to-protect-screens-from-unauthenticated-users">How to protect screens from unauthenticated users</a></li>
<li><a class="post-section-overview" href="#heading-how-to-interact-with-the-supabase-database">How to interact with the Supabase database</a></li>
<li><a class="post-section-overview" href="#heading-how-to-save-the-users-score-to-the-database">How to save user's score to the database</a></li>
<li><a class="post-section-overview" href="#heading-how-to-retrieve-data-from-supabase">How to retrieve data from Supabase</a></li>
<li><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></li>
</ul>
<h2 id="heading-mobile-application-demo">Mobile Application Demo</h2>
<p>To preview the application, download <a target="_blank" href="https://expo.dev/client">Expo Go</a> and paste the links below into the app URL field:</p>
<p><strong>Android:</strong> <code>exp://u.expo.dev/update/a4774250-e156-4d34-bcfc-a4f2549c2e1d</code><br><strong>iOS:</strong> <code>exp://u.expo.dev/update/7e5f8ba5-89c4-4c1d-b219-a613ace642df</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/app-demo.png" alt="Image" width="600" height="400" loading="lazy">
<em>Scan the QR code to preview the mobile quiz application within the Expo Go application</em></p>
<h2 id="heading-how-to-set-up-a-react-native-application-with-expo">How to Set Up a React Native Application with Expo</h2>
<p>Expo is an open-source platform that allows you to create cross-platform applications easily with JavaScript. It saves us from the complex configurations required to create a native application with the React Native CLI, making it the easiest and fastest way to build and publish React Native apps.</p>
<p>Execute the code snippet below to create a new <a target="_blank" href="https://expo.dev/">Expo</a> project that uses <a target="_blank" href="https://docs.expo.dev/router/introduction/">Expo Router</a> for navigating between screens.</p>
<pre><code class="lang-bash">npx create-expo-app@latest --template tabs@50
</code></pre>
<p><a target="_blank" href="https://docs.expo.dev/router/introduction/">Expo Router</a> is an open-source file-based routing system that enables users to navigate between screens easily. It is similar to Next.js, where each file name represents its route name.</p>
<p>Start the development server to ensure that the app is working as expected.</p>
<pre><code class="lang-bash">npx expo start
</code></pre>
<h3 id="heading-how-to-style-the-react-native-application-with-tailwind-css">How to style the React Native application with Tailwind CSS</h3>
<p>Tailwind CSS is a CSS framework that lets you create modern and stunning applications easily. </p>
<p>However, to style Expo applications using Tailwind CSS, you need to install <a target="_blank" href="https://www.nativewind.dev/v4/getting-started/expo-router">NativeWind</a> – a library that uses Tailwind CSS as its scripting language.</p>
<p>Run the code snippet below to install NativeWind and its dependencies:</p>
<pre><code class="lang-bash">npx expo install nativewind@^4.0.1 react-native-reanimated tailwindcss
</code></pre>
<p>Execute <code>npx tailwindcss init</code> within your terminal to create a <code>tailwind.config.js</code> file. Update the file with the code snippet below:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/** <span class="hljs-doctag">@type <span class="hljs-type">{import('tailwindcss').Config}</span> </span>*/</span>
<span class="hljs-built_in">module</span>.exports = {
    <span class="hljs-attr">content</span>: [<span class="hljs-string">"./app/**/*.{js,jsx,ts,tsx}"</span>],
    <span class="hljs-attr">presets</span>: [<span class="hljs-built_in">require</span>(<span class="hljs-string">"nativewind/preset"</span>)],
    <span class="hljs-attr">theme</span>: {
        <span class="hljs-attr">extend</span>: {},
    },
    <span class="hljs-attr">plugins</span>: [],
};
</code></pre>
<p>Create a <code>globals.css</code> file within the root of your project and add the Tailwind directives below:</p>
<pre><code class="lang-css"><span class="hljs-keyword">@tailwind</span> base;
<span class="hljs-keyword">@tailwind</span> components;
<span class="hljs-keyword">@tailwind</span> utilities;
</code></pre>
<p>Update the <code>babel.config.js</code> file with the code below:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">module</span>.exports = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">api</span>) </span>{
  api.cache(<span class="hljs-literal">true</span>);
  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">presets</span>: [
      [<span class="hljs-string">"babel-preset-expo"</span>, { <span class="hljs-attr">jsxImportSource</span>: <span class="hljs-string">"nativewind"</span> }],
      <span class="hljs-string">"nativewind/babel"</span>,
    ],
  };
};
</code></pre>
<p>Create a <code>metro.config.js</code> file within the root of your project and paste the code snippet below into the file:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { getDefaultConfig } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"expo/metro-config"</span>);
<span class="hljs-keyword">const</span> { withNativeWind } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'nativewind/metro'</span>);

<span class="hljs-keyword">const</span> config = getDefaultConfig(__dirname)

<span class="hljs-built_in">module</span>.exports = withNativeWind(config, { <span class="hljs-attr">input</span>: <span class="hljs-string">'./globals.css'</span> })
</code></pre>
<p>Finally, import the <code>./globals.css</code> file into the <code>app/_layout.tsx</code> file to enable you to style your application with Tailwind CSS:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//👉🏻 Within ./app/_layout.tsx</span>

<span class="hljs-keyword">import</span> <span class="hljs-string">"../globals.css"</span>;
</code></pre>
<p>Great job on creating the React Native project with Expo! Now, you're ready to add some style using Tailwind CSS. If you encounter any problems while installing NativeWind, check out the <a target="_blank" href="https://www.nativewind.dev/v4/getting-started/expo-router">documentation</a> for a step-by-step guide.</p>
<h2 id="heading-how-to-build-the-application-screens">How to Build the Application Screens</h2>
<p>Here, I'll guide you through building the application screens. They are divided into three categories:</p>
<ul>
<li>The Authentication screens – the register and login screens.</li>
<li>The Tab layout screens – the dashboard, leaderboard, and profile screens.</li>
<li>The Stack screens – the test and test completion screens.</li>
</ul>
<p>The application prompts new users to create an account and log in before allowing access to the Tab layout screens. </p>
<p>On the dashboard screen, users can take tests on various topics. The leaderboard screen showcases the top ten users. Users can log out or preview their previous attempts on the profile page.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/application-demo.gif" alt="Image" width="600" height="400" loading="lazy">
<em>Application Demo</em></p>
<h3 id="heading-how-to-build-the-authentication-screens">How to Build the Authentication Screens</h3>
<p>The authentication screens accept the user's email and password and ensure the credentials are valid before creating an account or granting access to the application.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/auth-screens.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Authentication Screens</em></p>
<p>Create an <code>index.tsx</code> and a <code>register.tsx</code> file within the <code>app</code> folder and a component that accepts the user's email and password using the <a target="_blank" href="https://reactnative.dev/docs/textinput">React Native TextInput</a> component.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Text, View, TextInput, Pressable, Alert } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> { Link, useRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">LoginScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> [email, setEmail] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
    <span class="hljs-keyword">const</span> [password, setPassword] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
    <span class="hljs-keyword">const</span> [loading, setLoading] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">const</span> router = useRouter();

    <span class="hljs-comment">//👇🏻 triggered when the user submits the email &amp; password</span>
    <span class="hljs-keyword">const</span> handleLogin = <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (!email.trim() || !password.trim())
            <span class="hljs-keyword">return</span> Alert.alert(<span class="hljs-string">"Error"</span>, <span class="hljs-string">"Please fill in all fields"</span>);
        setLoading(<span class="hljs-literal">true</span>);
        <span class="hljs-built_in">console</span>.log({
            email,
            password,
        });
        router.replace(<span class="hljs-string">"/(tabs)/"</span>);
    };

    <span class="hljs-keyword">return</span> (
        &lt;View&gt;
        {<span class="hljs-comment">/** -- user interface--*/</span>}
        &lt;/View&gt;
    );
}
</code></pre>
<p>The code snippet stores the user's email and password in states using the React useState hook. The <code>handleLogin</code> function accepts the user's email and password when the form is submitted and ensures that they are not empty before logging them to the console and redirecting the user to the Dashboard page.</p>
<p>You can create the user interface using the code snippet below. It displays the input fields for the user's credentials and an interactive Sign-in button that executes the <code>handleLogin</code> function. Additionally, the <code>loading</code> state ensures that the button is only pressed once.</p>
<pre><code class="lang-typescript">&lt;View className=<span class="hljs-string">' flex-1'</span>&gt;
    &lt;View className=<span class="hljs-string">'w-full px-4'</span>&gt;
        &lt;Text className=<span class="hljs-string">'text-3xl mb-4 font-bold text-white text-center'</span>&gt;
            Log <span class="hljs-keyword">in</span>
        &lt;/Text&gt;

        &lt;Text className=<span class="hljs-string">'text-lg text-gray-200'</span>&gt;Email Address&lt;/Text&gt;
        &lt;TextInput
            className=<span class="hljs-string">'w-full border-b-[1px] py-4 rounded-md mb-3 text-white font-bold'</span>
            value={email}
            onChangeText={setEmail}
        /&gt;
        &lt;Text className=<span class="hljs-string">'text-lg text-gray-200'</span>&gt;Password&lt;/Text&gt;
        &lt;TextInput
            className=<span class="hljs-string">'w-full border-b-[1px] py-4 rounded-md mb-3 text-white font-bold'</span>
            secureTextEntry
            value={password}
            onChangeText={setPassword}
        /&gt;
        &lt;Pressable
            className={<span class="hljs-string">`w-full <span class="hljs-subst">${
                loading ? <span class="hljs-string">"bg-orange-200"</span> : <span class="hljs-string">"bg-orange-600"</span>
            }</span> rounded-xl p-4 border-[1px] border-orange-200`</span>}
            disabled={loading}
            onPress={<span class="hljs-function">() =&gt;</span> handleLogin()}
        &gt;
            &lt;Text className=<span class="hljs-string">'text-white text-center font-bold text-xl'</span>&gt;
                {loading ? <span class="hljs-string">"Authenticating..."</span> : <span class="hljs-string">"Sign in"</span>}
            &lt;/Text&gt;
        &lt;/Pressable&gt;
        &lt;Text className=<span class="hljs-string">'text-center mt-2 text-orange-200'</span>&gt;
            Don<span class="hljs-string">'t have an account?{" "}
            &lt;Link href='</span>/register<span class="hljs-string">'&gt;
                &lt;Text className='</span>text-white<span class="hljs-string">'&gt;Register&lt;/Text&gt;
            &lt;/Link&gt;
        &lt;/Text&gt;
    &lt;/View&gt;
&lt;/View&gt;</span>
</code></pre>
<p>For instance, the <code>loading</code> state becomes true when a user clicks the Sign-in button. The Pressable component (button) has a <code>disabled</code> attribute set to the <code>loading</code> state to ensure that the user does not press the button multiple times. Additionally, you can use the loading state to notify the user that the request is processing.</p>
<p>The <code>register.tsx</code> file is also similar to the <code>login.tsx</code> file. You only need to change the words from Login to Register.</p>
<h3 id="heading-how-to-build-the-tab-screens">How to Build the Tab Screens</h3>
<p>The Tab Screens consist of the Dashboard, Leaderboard, and Profile screens.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/tab-screens.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Tab Screens</em></p>
<p>Create a <code>(tabs)</code> folder containing <code>index.tsx</code>, <code>leaderboard.tsx</code>, <code>profile.tsx</code>, and <code>_layout.tsx</code> files within the <code>app</code> folder.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> app
mkdir (tabs)
<span class="hljs-built_in">cd</span> (tabs)
touch index.tsx leaderboard.tsx profile.tsx _layout.tsx
</code></pre>
<p>After creating the <code>_layout.tsx</code> file within the (tabs) folder, update the <code>_layout.tsx</code> to specify Tab screen navigation for the newly created screens. The screens use icons from the <a target="_blank" href="https://icons.expo.fyi/Index">Expo Vector Icons library</a>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Tabs } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;
<span class="hljs-keyword">import</span> { Ionicons, MaterialIcons, FontAwesome5 } <span class="hljs-keyword">from</span> <span class="hljs-string">"@expo/vector-icons"</span>;
<span class="hljs-keyword">import</span> { ActivityIndicator } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TabScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> (
        &lt;Tabs
            screenOptions={{
                tabBarActiveTintColor: <span class="hljs-string">"#f97316"</span>,
                tabBarInactiveTintColor: <span class="hljs-string">"gray"</span>,
                tabBarShowLabel: <span class="hljs-literal">false</span>,
                headerShown: <span class="hljs-literal">false</span>,
                tabBarStyle: {
                    backgroundColor: <span class="hljs-string">"#ffedd5"</span>,
                    borderTopColor: <span class="hljs-string">"#ffedd5"</span>,
                },
            }}
        &gt;
            &lt;Tabs.Screen
                name=<span class="hljs-string">'index'</span>
                options={{
                    tabBarIcon: <span class="hljs-function">(<span class="hljs-params">{ color }</span>) =&gt;</span> (
                        &lt;Ionicons name=<span class="hljs-string">'home'</span> size={<span class="hljs-number">24</span>} color={color} /&gt;
                    ),
                }}
            /&gt;
            &lt;Tabs.Screen
                name=<span class="hljs-string">'leaderboard'</span>
                options={{
                    tabBarIcon: <span class="hljs-function">(<span class="hljs-params">{ color }</span>) =&gt;</span> (
                        &lt;MaterialIcons name=<span class="hljs-string">'leaderboard'</span> size={<span class="hljs-number">24</span>} color={color} /&gt;
                    ),
                }}
            /&gt;
            &lt;Tabs.Screen
                name=<span class="hljs-string">'profile'</span>
                options={{
                    tabBarIcon: <span class="hljs-function">(<span class="hljs-params">{ color }</span>) =&gt;</span> (
                        &lt;FontAwesome5 name=<span class="hljs-string">'user-alt'</span> size={<span class="hljs-number">24</span>} color={color} /&gt;
                    ),
                }}
            /&gt;
        &lt;/Tabs&gt;
    );
}
</code></pre>
<p>Next, update the <code>RootLayoutNav</code> component within the <code>_app/layout.tsx</code> file to render all the screens within the application.</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayoutNav</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> (
        &lt;Stack screenOptions={{ headerShown: <span class="hljs-literal">false</span> }}&gt;
            &lt;Stack.Screen name=<span class="hljs-string">'(tabs)'</span> /&gt;
            &lt;Stack.Screen name=<span class="hljs-string">'(stack)'</span> /&gt;
            &lt;Stack.Screen name=<span class="hljs-string">'index'</span> /&gt;
            &lt;Stack.Screen name=<span class="hljs-string">'register'</span> /&gt;
        &lt;/Stack&gt;
    );
}
</code></pre>
<h4 id="heading-the-dashboard-screen">The Dashboard Screen</h4>
<p>Update the component to allow users to select four categories from a list of categories.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">HomeScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> greet = getGreeting();
    <span class="hljs-keyword">const</span> router = useRouter();
    <span class="hljs-keyword">const</span> { session } = useAuth();
    <span class="hljs-keyword">const</span> [loading, setLoading] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">const</span> [userCategories, setUserCategories] = useState&lt;<span class="hljs-built_in">string</span>[]&gt;([]);

    <span class="hljs-keyword">const</span> fetchQuestions = <span class="hljs-keyword">async</span> () =&gt; {};

    <span class="hljs-keyword">const</span> handleStartTest = <span class="hljs-keyword">async</span> () =&gt; {
        Alert.alert(<span class="hljs-string">"Start Test"</span>, <span class="hljs-string">"Are you sure you want to start the test?"</span>, [
            {
                text: <span class="hljs-string">"Cancel"</span>,
                style: <span class="hljs-string">"destructive"</span>,
            },
            {
                text: <span class="hljs-string">"Yes"</span>,
                onPress: <span class="hljs-function">() =&gt;</span> fetchQuestions(),
            },
        ]);
    };

    <span class="hljs-keyword">return</span> (
        &lt;SafeAreaView className=<span class="hljs-string">'flex-1 bg-orange-100 px-4 py-2'</span>&gt;
            &lt;View className=<span class="hljs-string">'flex flex-row items-center justify-between mb-2'</span>&gt;
                &lt;View&gt;
                    &lt;Text className=<span class="hljs-string">'font-bold text-2xl mb-[1px]'</span>&gt;
                        Good morning
                        &lt;Ionicons name=<span class="hljs-string">'partly-sunny-sharp'</span> size={<span class="hljs-number">24</span>} color=<span class="hljs-string">'orange'</span> /&gt;
                    &lt;/Text&gt;

                    &lt;Text className=<span class="hljs-string">'text-lg'</span>&gt;Welcome User&lt;/Text&gt;
                &lt;/View&gt;
            &lt;/View&gt;
            {userCategories.length === <span class="hljs-number">4</span> &amp;&amp; (
                &lt;Pressable
                    className={<span class="hljs-string">`w-full h-[70px] flex items-center justify-center <span class="hljs-subst">${
                        loading ? <span class="hljs-string">"bg-orange-300"</span> : <span class="hljs-string">"bg-orange-500"</span>
                    }</span> rounded-xl mb-2`</span>}
                    disabled={loading}
                    onPress={<span class="hljs-function">() =&gt;</span> handleStartTest()}
                &gt;
                    &lt;Text className=<span class="hljs-string">'text-xl font-bold text-orange-50'</span>&gt;
                        {loading ? <span class="hljs-string">"Loading questions..."</span> : <span class="hljs-string">"START TEST"</span>}
                    &lt;/Text&gt;
                &lt;/Pressable&gt;
            )}

            &lt;View className=<span class="hljs-string">'w-full flex-1'</span>&gt;
                &lt;Text className=<span class="hljs-string">'text-xl font-bold text-orange-500 mb-4'</span>&gt;
                    Available Categories
                &lt;/Text&gt;
                &lt;FlatList
                    data={categories}
                    numColumns={<span class="hljs-number">2</span>}
                    contentContainerStyle={{ width: <span class="hljs-string">"100%"</span>, gap: <span class="hljs-number">10</span> }}
                    columnWrapperStyle={{ gap: <span class="hljs-number">10</span> }}
                    renderItem={<span class="hljs-function">(<span class="hljs-params">{ item }</span>) =&gt;</span> (
                        &lt;Categories
                            item={item}
                            userCategories={userCategories}
                            setUserCategories={setUserCategories}
                        /&gt;
                    )}
                    showsVerticalScrollIndicator={<span class="hljs-literal">false</span>}
                    keyExtractor={<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> item.id}
                /&gt;
            &lt;/View&gt;
        &lt;/SafeAreaView&gt;
    );
}
</code></pre>
<p>The code snippet above renders a list of categories where users can select only four categories to answer questions on and start the quiz.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/dashboard-screen.gif" alt="Image" width="600" height="400" loading="lazy">
<em>The Dashboard Screen</em></p>
<h4 id="heading-the-leaderboard-screen">The Leaderboard Screen</h4>
<p>The Leaderboard screen displays the top ten users ranked in descending order.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Text, FlatList, SafeAreaView } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> Board <span class="hljs-keyword">from</span> <span class="hljs-string">"../../components/Board"</span>;

<span class="hljs-keyword">interface</span> Props {
    total_score: <span class="hljs-built_in">number</span>;
    user_id: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">LeaderboardScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> [leaderboard, setLeaderboard] = useState&lt;Props[]&gt;([]);

    <span class="hljs-keyword">return</span> (
        &lt;SafeAreaView className=<span class="hljs-string">'flex-1 bg-orange-100 p-4'</span>&gt;
            &lt;Text className=<span class="hljs-string">'text-2xl font-bold text-gray-500 text-center mb-6'</span>&gt;
                Leaderboard
            &lt;/Text&gt;

            &lt;FlatList
                data={leaderboard}
                renderItem={<span class="hljs-function">(<span class="hljs-params">{ item }</span>) =&gt;</span> &lt;Board item={item} /&gt;}
                keyExtractor={<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> item.user_id}
                showsVerticalScrollIndicator={<span class="hljs-literal">false</span>}
            /&gt;
        &lt;/SafeAreaView&gt;
    );
}
</code></pre>
<p>The code snippet above renders a FlatList with ten items. You can create an array containing ten users and pass it into the FlatList for now.</p>
<h4 id="heading-the-profile-screen">The Profile Screen</h4>
<p>The Profile Screen displays the user's image, recent attempts, and a log-out button that enables the user to sign out of the application.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ProfileScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> [loading, setLoading] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">const</span> [total_score, setTotalScore] = useState&lt;<span class="hljs-built_in">number</span>&gt;(<span class="hljs-number">0</span>);
    <span class="hljs-keyword">const</span> [attempts, setAttempts] = useState&lt;<span class="hljs-built_in">string</span>[]&gt;([]);

    <span class="hljs-keyword">const</span> handleSignOut = <span class="hljs-keyword">async</span> () =&gt; {
        setLoading(<span class="hljs-literal">true</span>);
    };

    <span class="hljs-keyword">return</span> (
        &lt;SafeAreaView className=<span class="hljs-string">'flex-1 bg-orange-100 p-4'</span>&gt;
            &lt;View className=<span class="hljs-string">'flex items-center justify-center mb-6'</span>&gt;
                &lt;Text className=<span class="hljs-string">'text-gray-600 mb-[1px]'</span>&gt;
                    &lt;FontAwesome name=<span class="hljs-string">'star'</span> size={<span class="hljs-number">20</span>} color=<span class="hljs-string">'red'</span> /&gt;
                    &lt;Text&gt;<span class="hljs-number">45</span>&lt;/Text&gt;
                &lt;/Text&gt;
                &lt;Text className=<span class="hljs-string">'text-gray-600 mb-2'</span>&gt;<span class="hljs-meta">@dhastix</span>&lt;/Text&gt;

                &lt;Pressable onPress={<span class="hljs-function">() =&gt;</span> handleSignOut()} disabled={loading}&gt;
                    &lt;Text className=<span class="hljs-string">'text-red-500'</span>&gt;
                        {loading ? <span class="hljs-string">"Logging out..."</span> : <span class="hljs-string">"Log out"</span>}
                    &lt;/Text&gt;
                &lt;/Pressable&gt;
            &lt;/View&gt;

            &lt;Text className=<span class="hljs-string">'font-bold text-xl text-gray-700 mb-3 px-4'</span>&gt;
                Recent Attempts
            &lt;/Text&gt;

            &lt;FlatList
                data={attempts}
                contentContainerStyle={{ padding: <span class="hljs-number">15</span> }}
                renderItem={<span class="hljs-function">(<span class="hljs-params">{ item }</span>) =&gt;</span> &lt;Attempts item={item} /&gt;}
                keyExtractor={<span class="hljs-function">(<span class="hljs-params">item, index</span>) =&gt;</span> index.toString()}
                showsVerticalScrollIndicator={<span class="hljs-literal">false</span>}
            /&gt;
        &lt;/SafeAreaView&gt;
    );
}
</code></pre>
<p>The code snippet above displays the user's image, the sign-out button, and all the user's attempts. You can create an array of items for testing purposes.</p>
<h3 id="heading-how-to-build-the-stack-screens">How to Build the Stack Screens</h3>
<p>The Stack Screens comprise two screens – the quiz screen and the screen that displays the user's score after completing a quiz session.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/test-screens-1.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Stack Screens</em></p>
<h4 id="heading-the-quiz-screen">The Quiz Screen</h4>
<p>The Quiz Screen displays a timer that countdowns from 15 seconds before moving to the next question. It shows the question, its category, available options, the Skip and Next buttons, and a cancel icon.</p>
<p>Create a similar screen to the one shown below. You can use <a target="_blank" href="https://github.com/dha-stix/techtest-app/blob/main/app/(stack)/test.tsx">this</a> as a guide.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/test-screen.gif" alt="Image" width="600" height="400" loading="lazy">
<em>The Quiz Screen</em></p>
<h4 id="heading-the-quiz-completion-screen">The Quiz Completion Screen</h4>
<p>It displays the user's score after completing a test.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {
    SafeAreaView,
    Text,
    Pressable,
    View,
    ImageBackground,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> { MaterialIcons } <span class="hljs-keyword">from</span> <span class="hljs-string">"@expo/vector-icons"</span>;
<span class="hljs-keyword">import</span> { useLocalSearchParams } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">CompletedScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> { score } = useLocalSearchParams();

    <span class="hljs-keyword">return</span> (
        &lt;View className=<span class="hljs-string">'flex flex-1 bg-orange-400'</span>&gt;
            &lt;ImageBackground
                source={{ uri: <span class="hljs-string">"https://source.unsplash.com/NAP14GEjvh8"</span> }}
                className=<span class="hljs-string">'flex-1 p-4'</span>
            &gt;
                &lt;SafeAreaView /&gt;
                &lt;Pressable onPress={<span class="hljs-function">() =&gt;</span> router.replace(<span class="hljs-string">"/(tabs)/"</span>)}&gt;
                    &lt;MaterialIcons name=<span class="hljs-string">'cancel'</span> size={<span class="hljs-number">60</span>} color=<span class="hljs-string">'white'</span> /&gt;
                &lt;/Pressable&gt;

                &lt;View className=<span class="hljs-string">'flex-1 flex items-center justify-center'</span>&gt;
                    &lt;View className=<span class="hljs-string">'bg-orange-50 w-full py-[50px] rounded-xl p-4 flex items-center justify-center shadow-lg shadow-orange-500'</span>&gt;
                        &lt;Text className=<span class="hljs-string">'text-3xl text-orange-600 font-bold mb-4'</span>&gt;
                            {<span class="hljs-built_in">Number</span>(score) &gt; <span class="hljs-number">20</span> ? <span class="hljs-string">"Congratulations🥳"</span> : <span class="hljs-string">"Sorry! You lose 🥲"</span>}
                        &lt;/Text&gt;
                        &lt;Text className=<span class="hljs-string">'font-bold text-xl'</span>&gt;You scored {score}!&lt;/Text&gt;
                    &lt;/View&gt;
                &lt;/View&gt;
            &lt;/ImageBackground&gt;
        &lt;/View&gt;
    );
}
</code></pre>
<p>The code snippet above accepts the user's score as a parameter after completing the quiz and displays the score to the user.</p>
<h2 id="heading-how-to-generate-quiz-questions-and-answers-from-chatgpt">How to Generate Quiz Questions and Answers from ChatGPT</h2>
<p>When building a quiz application, the first question is: how do you get the questions and options for the application? You can either create a list of questions or search for a suitable public API.</p>
<p>However, I'll guide you through creating a list of questions and options in JSON format using ChatGPT. Use this prompt to generate questions and answers from ChatGPT:</p>
<blockquote>
<p><em>Generate 25 distinct questions on  and ensure they are in JSON format containing an id, category which is , a question attribute containing the question, an options array of 3 options, and an answer property.</em></p>
</blockquote>
<p>The prompt returns a JSON result containing the questions and answers. You can host them on GitHub or save them to a database.</p>
<p>The questions and answers I'm using in this mobile application are available on <a target="_blank" href="https://github.com/dha-stix/trivia-app/tree/main/questions">GitHub</a>. Feel free to clone or copy the files.</p>
<p>Once your questions and answers are ready, you can connect the application to Supabase.</p>
<h2 id="heading-how-to-add-supabase-to-react-native">How to Add Supabase to React Native</h2>
<p>Supabase is an open-source Firebase alternative that enables you to create secured and scalable software applications within a few minutes.</p>
<p>It provides a secured Postgres database, a complete user management system that handles various forms of authentication (including email and password, email sign-in, and social authentication), a file storage system that lets you store and serve files of any size, real-time communication, and many other features.</p>
<p>In this tutorial, I'll walk you through the following:</p>
<ul>
<li>How to authenticate users and control access to some application screens with Supabase.</li>
<li>How to save the users' scores to the database to enable you to rank them based on their scores.</li>
</ul>
<p>First, you need to install Supabase and its required dependencies. You can do that with the following commands:</p>
<pre><code class="lang-bash">npm install @supabase/supabase-js 
npm install react-native-elements @react-native-async-storage/async-storage react-native-url-polyfill
npx expo install expo-secure-store
</code></pre>
<p>Create a <code>supabase.ts</code> file within your project and copy the code snippet below into the file to initiate Supabase:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-string">"react-native-url-polyfill/auto"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> SecureStore <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-secure-store"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/supabase-js"</span>;

<span class="hljs-keyword">const</span> ExpoSecureStoreAdapter = {
    getItem: <span class="hljs-function">(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> SecureStore.getItemAsync(key);
    },
    setItem: <span class="hljs-function">(<span class="hljs-params">key: <span class="hljs-built_in">string</span>, value: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
        SecureStore.setItemAsync(key, value);
    },
    removeItem: <span class="hljs-function">(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
        SecureStore.deleteItemAsync(key);
    },
};

<span class="hljs-keyword">const</span> supabaseUrl = <span class="hljs-string">"YOUR_REACT_NATIVE_SUPABASE_URL"</span>;
<span class="hljs-keyword">const</span> supabaseAnonKey = <span class="hljs-string">"YOUR_REACT_NATIVE_SUPABASE_ANON_KEY"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> supabase = createClient(supabaseUrl, supabaseAnonKey, {
    auth: {
        storage: ExpoSecureStoreAdapter <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>,
        autoRefreshToken: <span class="hljs-literal">true</span>,
        persistSession: <span class="hljs-literal">true</span>,
        detectSessionInUrl: <span class="hljs-literal">false</span>,
    },
});
</code></pre>
<p>Next, visit the <a target="_blank" href="https://supabase.com">Supabase homepage</a>, sign in, and create a new organization and project.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/project.png" alt="Image" width="600" height="400" loading="lazy">
<em>Create a new Supabase project</em></p>
<p>Click the Settings icon on the sidebar and select API to copy the project URL and the public API key.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-24-at-17.50.34.png" alt="Image" width="600" height="400" loading="lazy">
<em>Supabase API settings containing project credentials</em></p>
<p>Create a <code>.env.local</code> file and copy the credentials into the variables. Update the <code>supabase.ts</code> file to use the Supabase URL and API key.</p>
<pre><code class="lang-txt">EXPO_PUBLIC_API_URL=&lt;YOUR_SUPABASE_URL&gt;
EXPO_PUBLIC_API_KEY=&lt;YOUR_SUPABASE_API_KEY&gt;
</code></pre>
<p>Congratulations! You can now interact with Supabase from your application and access various features such as authentication, database, file storage, and so on.</p>
<h2 id="heading-how-to-add-supabase-authentication-to-react-native-applications">How to Add Supabase Authentication to React Native Applications</h2>
<p>Supabase offers various forms of authentication. But we only need the email and password method of authentication for this application.</p>
<h3 id="heading-how-to-sign-up-new-users">How to sign up new users</h3>
<p>The code snippet below accepts an email and a password and creates an account for the user. Otherwise, it returns an error if any of the credentials is invalid.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//👇🏻 import supabase from supabase file</span>
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/supabase"</span>;

<span class="hljs-comment">//👇🏻 sign up function</span>
<span class="hljs-keyword">const</span> handleRegister = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!email.trim() || !password.trim())
        <span class="hljs-keyword">return</span> Alert.alert(<span class="hljs-string">"Error"</span>, <span class="hljs-string">"Please fill in all fields"</span>);
    <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signUp({ email, password });
    <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">return</span> Alert.alert(<span class="hljs-string">"Error"</span>, error.message);
    router.replace(<span class="hljs-string">"/"</span>);
};
</code></pre>
<p>With the <code>supabase.auth.signUp()</code> function, Supabase handles the authentication process. If successful, the user is redirected to the login page. Otherwise, it displays an error message.</p>
<h3 id="heading-how-to-sign-in-existing-users">How to sign in existing users</h3>
<p>This function allows existing users to access the application. It accepts the user's email and password and logs the user into the application.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//👇🏻 import supabase from supabase file</span>
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/supabase"</span>;

<span class="hljs-comment">//👇🏻 register function</span>
<span class="hljs-keyword">const</span> handleLogin = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!email.trim() || !password.trim())
        <span class="hljs-keyword">return</span> Alert.alert(<span class="hljs-string">"Error"</span>, <span class="hljs-string">"Please fill in all fields"</span>);
    <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signInWithPassword({ email, password });
    <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">return</span> Alert.alert(<span class="hljs-string">"Error"</span>, error.message);
    router.replace(<span class="hljs-string">"/(tabs)/"</span>);
};
</code></pre>
<p>The <code>supabase.auth.signInWithPassword()</code> function validates the user's email and password and redirects the user to the Dashboard screen. Otherwise, it returns the necessary authentication error.</p>
<h3 id="heading-how-to-log-users-out-of-the-application">How to log users out of the application</h3>
<p>Supabase also allows users to sign out of the application. You can execute this function when the user clicks a button within the Profile page.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//👇🏻 import supabase from supabase file</span>
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/supabase"</span>;

<span class="hljs-comment">//👇🏻 sign out function</span>
<span class="hljs-keyword">const</span> handleSignOut = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signOut();
        <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-built_in">console</span>.log(error);
    }
};
</code></pre>
<h3 id="heading-how-to-protect-screens-from-unauthenticated-users">How to protect screens from unauthenticated users</h3>
<p>You've been able to add the sign-up, sign-in, and log-out functionalities to the React Native application. But the Dashboard and other screens containing sensitive data are still accessible to unauthenticated users.</p>
<p>How do we fix this?</p>
<p>In this section, I'll walk you through how to protect screens from unauthorized users using the <a target="_blank" href="https://react.dev/reference/react/createContext">React Context API</a>.</p>
<p>The React Context API allows us to pass data through the component tree without needing to pass props down manually at every level.</p>
<p>Create an <code>AuthProvider.tsx</code> file. This is where the data to be passed down the application screens is stored. Copy the code snippet below into the file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"./supabase"</span>;
<span class="hljs-keyword">import</span> { Session } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/supabase-js"</span>;
<span class="hljs-keyword">import</span> {
    PropsWithChildren,
    createContext,
    useContext,
    useEffect,
    useState,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">type</span> AuthData = {
    session: Session | <span class="hljs-literal">null</span>;
    loading: <span class="hljs-built_in">boolean</span>;
};

<span class="hljs-comment">//👇🏻 data to be passed down the components</span>
<span class="hljs-keyword">const</span> AuthContext = createContext&lt;AuthData&gt;({
    session: <span class="hljs-literal">null</span>,
    loading: <span class="hljs-literal">true</span>,
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">AuthProvider</span>(<span class="hljs-params">{ children }: PropsWithChildren</span>) </span>{
    <span class="hljs-keyword">const</span> [session, setSession] = useState&lt;Session | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
    <span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">true</span>);

    <span class="hljs-comment">//👇🏻 fetches the current user's session</span>
    useEffect(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">const</span> fetchSession = <span class="hljs-keyword">async</span> () =&gt; {
            <span class="hljs-keyword">const</span> {
                data: { session },
            } = <span class="hljs-keyword">await</span> supabase.auth.getSession();
            setSession(session);
            setLoading(<span class="hljs-literal">false</span>);
        };

        fetchSession();
        supabase.auth.onAuthStateChange(<span class="hljs-function">(<span class="hljs-params">_event, session</span>) =&gt;</span> {
            setSession(session);
            setLoading(<span class="hljs-literal">false</span>);
        });
    }, []);

    <span class="hljs-keyword">return</span> (
        &lt;AuthContext.Provider value={{ session, loading }}&gt;
            {children}
        &lt;/AuthContext.Provider&gt;
    );
}
<span class="hljs-comment">//👇🏻 custom hook for using the context (data)</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useAuth = <span class="hljs-function">() =&gt;</span> useContext(AuthContext);
</code></pre>
<p>The code snippet retrieves the current user's session. If the user is signed in, the session and loading state variables are updated to show that the user is active, and they are passed into other components within the application.</p>
<p>The <code>useAuth</code> custom hook allows you to access the state variables (session and loading) within the application screens.</p>
<p>To access the context (data) available within the application screens, wrap the entire application with the <code>AuthProvider</code>. So now, update the <code>RootLayoutNav</code> component within the <code>app/_layout.tsx</code> file as shown below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> AuthProvider <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/AuthProvider"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayoutNav</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> (
        &lt;AuthProvider&gt;
            &lt;Stack screenOptions={{ headerShown: <span class="hljs-literal">false</span> }}&gt;
                &lt;Stack.Screen name=<span class="hljs-string">'(tabs)'</span> /&gt;
                &lt;Stack.Screen name=<span class="hljs-string">'(stack)'</span> /&gt;
                &lt;Stack.Screen name=<span class="hljs-string">'index'</span> /&gt;
                &lt;Stack.Screen name=<span class="hljs-string">'register'</span> /&gt;
            &lt;/Stack&gt;
        &lt;/AuthProvider&gt;
    );
}
</code></pre>
<p>Congratulations! You've successfully set up the context. Next, how do we read the context and ensure that only authenticated users can view some of the application screens?</p>
<p>You can do this using the custom <code>useAuth</code> hook. For example, you can protect the Tabs screens via the <code>(tabs)/_layout.tsx</code> file.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Tabs, Redirect } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;
<span class="hljs-keyword">import</span> { useAuth } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../lib/AuthProvider"</span>;
<span class="hljs-keyword">import</span> { ActivityIndicator } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TabScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> { session, loading } = useAuth();

    <span class="hljs-keyword">if</span> (!session) {
        <span class="hljs-keyword">return</span> &lt;Redirect href=<span class="hljs-string">'/'</span> /&gt;;
    }

    <span class="hljs-keyword">if</span> (loading) {
        <span class="hljs-keyword">return</span> &lt;ActivityIndicator size=<span class="hljs-string">'large'</span> color=<span class="hljs-string">'#f97316'</span> /&gt;;
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">return</span> (
            &lt;Tabs
                screenOptions={{
                    tabBarActiveTintColor: <span class="hljs-string">"#f97316"</span>,
                    tabBarInactiveTintColor: <span class="hljs-string">"gray"</span>,
                    tabBarShowLabel: <span class="hljs-literal">false</span>,
                    headerShown: <span class="hljs-literal">false</span>,
                    tabBarStyle: {
                        backgroundColor: <span class="hljs-string">"#ffedd5"</span>,
                        borderTopColor: <span class="hljs-string">"#ffedd5"</span>,
                    },
                }}
            &gt;
                {<span class="hljs-comment">/**-- screens--*/</span>}
            &lt;/Tabs&gt;
        );
    }
}
</code></pre>
<p>The code snippet above checks if there is a session for the current user. If null, the application redirects the user to the login screen. If the application is yet to determine the user's status, it displays a loading icon.</p>
<h2 id="heading-how-to-interact-with-the-supabase-database">How to Interact with the Supabase Database</h2>
<p>In this section, I'll walk you through creating the database for the mobile application. You'll learn how to store and retrieve the user's scores and rank them based on their total score.</p>
<p>Before we proceed, note that the application calculates the user's score after answering each question on the test screen. Upon completion, the user's score is retrieved and displayed on the test completion screen.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> handleSave = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-comment">//👇🏻 checks if the user has not completed the test</span>
    <span class="hljs-keyword">if</span> (count &lt; questions.length - <span class="hljs-number">1</span>) {
        <span class="hljs-comment">//👇🏻 updates the user's score if the selected answer is correct</span>
        <span class="hljs-keyword">if</span> (questions[count].answer === userAnswer) {
            setUserScore(<span class="hljs-function">(<span class="hljs-params">userScore</span>) =&gt;</span> userScore + <span class="hljs-number">1</span>);
        }
        <span class="hljs-comment">//👇🏻 change the question, refresh the selected answer and time</span>
        setCount(<span class="hljs-function">(<span class="hljs-params">count</span>) =&gt;</span> count + <span class="hljs-number">1</span>);
        setSelectedBox(<span class="hljs-literal">null</span>);
        setTime(<span class="hljs-number">15</span>);
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">//👇🏻 test completed</span>
        router.push({
            pathname: <span class="hljs-string">"/(stack)/completed"</span>,
            params: { score: userScore },
        });
    }
};
</code></pre>
<p>Within your Supabase project, select Table Editor from the sidebar menu and create a new table containing the following columns:</p>
<ul>
<li><code>id</code> – contains a unique ID for each row of data.</li>
<li><code>created_at</code> – represents the time the data was created.</li>
<li><code>attempts</code> – a text array containing the score and date attributes.</li>
<li><code>total_score</code> – represents a user's cumulative score. We'll rank users using this score</li>
<li><code>user_id</code> – a unique ID used to identify each user's data.</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-24-at-10.05.55.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Table Columns</em></p>
<p>Finally, you can add a Row Level Security that allows only authenticated users interact with the database.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-24-at-10.20.51.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Table Row Level Security Policy</em></p>
<h3 id="heading-how-to-save-the-users-score-to-the-database">How to save the user's score to the database</h3>
<p>Before you can save a user's score to the database, you need to check if the user's data already exists – meaning the user has taken a test before. If true, you need to update the user's score with the latest test score. Otherwise, add the data to the database.</p>
<p>The code snippet below accepts the user's score and user's ID (from the session data) and saves the user's score to Supabase.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> saveScore = <span class="hljs-keyword">async</span> (userScore: <span class="hljs-built_in">number</span>, userID: <span class="hljs-built_in">string</span>) =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-comment">//👇🏻 check if the user data exists</span>
        <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
            .from(<span class="hljs-string">"scores"</span>)
            .select()
            .eq(<span class="hljs-string">"user_id"</span>, userID);
        <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;

        <span class="hljs-comment">//👇🏻 if the user data does not exist, insert a new one</span>
        <span class="hljs-keyword">if</span> (error || !data.length) {
            <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
                .from(<span class="hljs-string">"scores"</span>)
                .insert({
                    attempts: [{ score: userScore, date: getCurrentDate() }],
                    total_score: userScore,
                    user_id: userID,
                })
                .single();
            <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-comment">//👇🏻 if the user data exists, update the attempts and total_score</span>
            <span class="hljs-keyword">const</span> { data: updateData, error } = <span class="hljs-keyword">await</span> supabase
                .from(<span class="hljs-string">"scores"</span>)
                .update({
                    attempts: [
                        ...data[<span class="hljs-number">0</span>].attempts,
                        { score: userScore, date: getCurrentDate() },
                    ],
                    total_score: data[<span class="hljs-number">0</span>].total_score + userScore,
                })
                .eq(<span class="hljs-string">"user_id"</span>, userID);
            <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
        }
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.log(err);
    }
};
</code></pre>
<h3 id="heading-how-to-retrieve-data-from-supabase">How to retrieve data from Supabase</h3>
<p>Recall that you need to rank the users based on their scores on the Leaderboard screen and retrieve the user's attempts on the Profile screen.</p>
<p>The code snippet below accepts a user's ID and retrieves the attempts and total score from the database.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getUserAttempts = <span class="hljs-keyword">async</span> (userID: <span class="hljs-built_in">string</span>) =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
            .from(<span class="hljs-string">"scores"</span>)
            .select(<span class="hljs-string">"attempts, total_score"</span>)
            .eq(<span class="hljs-string">"user_id"</span>, userID);
        <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
        <span class="hljs-keyword">return</span> { attempts: data[<span class="hljs-number">0</span>].attempts, total_score: data[<span class="hljs-number">0</span>].total_score };
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-keyword">return</span> { attempts: <span class="hljs-string">""</span>, total_score: <span class="hljs-number">0</span> };
    }
};
</code></pre>
<p>The code snippet below retrieves the top ten users from the database based on their score.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getLeaderBoard = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
            .from(<span class="hljs-string">"scores"</span>)
            .select(<span class="hljs-string">"total_score, user_id"</span>)
            .order(<span class="hljs-string">"total_score"</span>, { ascending: <span class="hljs-literal">false</span> })
            .limit(<span class="hljs-number">10</span>);
        <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
        <span class="hljs-keyword">return</span> data;
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }
};
</code></pre>
<p>Congratulations! You've successfully completed the project for this tutorial.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you’ve learned how to:</p>
<ul>
<li>build React Native mobile applications with Expo,</li>
<li>style your mobile applications with <a target="_blank" href="https://www.nativewind.dev/">Tailwind CSS</a>,</li>
<li>create stack and tab screen navigations using <a target="_blank" href="https://docs.expo.dev/router/introduction/">Expo Router</a>,</li>
<li>use Supabase and leverage its authentication and database features to build full-stack applications.</li>
</ul>
<p>Supabase is an amazing tool that enables you to build a full-stack software application with no hassle. If you are looking forward to shipping great software products or side projects faster, consider using Supabase.</p>
<p>Expo also saves us from the complexities of setting up and developing mobile applications using the <a target="_blank" href="https://reactnative.dev/docs/environment-setup">React Native CLI</a>. It enables you to focus more on building your applications while it handles the necessary configurations, including deployment.</p>
<p>Feel free to customise the application using <a target="_blank" href="https://chat.openai.com/">ChatGPT</a> to generate questions and answers tailored to any niche or topic.</p>
<p>The source code for this tutorial is available in this <a target="_blank" href="https://github.com/dha-stix/techtest-app">GitHub repository</a>.</p>
<p>Thank you for reading!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Add Tailwind CSS to Your React Native Expo App ]]>
                </title>
                <description>
                    <![CDATA[ Tailwind CSS has been quite popular in the web development world due to its utility-first approach and seamless integration.  However, when developing mobile apps with React Native, integrating Tailwind CSS may be challenging. But guess what? Not any... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/tailwindcss-in-react-native-expo/</link>
                <guid isPermaLink="false">66b9ee776a5986e4892f960b</guid>
                
                    <category>
                        <![CDATA[ CSS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ CSS3 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tailwind ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ John Caleb ]]>
                </dc:creator>
                <pubDate>Tue, 27 Feb 2024 09:22:11 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/02/FREECODE-CAMP-DEFAULT-1-.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Tailwind CSS has been quite popular in the web development world due to its utility-first approach and seamless integration. </p>
<p>However, when developing mobile apps with React Native, integrating Tailwind CSS may be challenging. But guess what? Not anymore. With the development of tools such as <a target="_blank" href="https://www.nativewind.dev/">NativeWind</a>, React Native developers can leverage Tailwind CSS power to design stunning and responsive mobile UIs easily.</p>
<p>In this tutorial, you'll learn the process of integrating Tailwind CSS to your React Native <a target="_blank" href="https://expo.io/">Expo</a> app using NativeWind. We'll also build a simple login screen with NativeWind.</p>
<h2 id="heading-table-of-contents">Table of Contents:</h2>
<ul>
<li><a class="post-section-overview" href="#heading-whats-nativewind">What's NativeWind?</a></li>
<li><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></li>
<li><a class="post-section-overview" href="#heading-getting-started">Getting Started</a> </li>
<li><a class="post-section-overview" href="#heading-how-to-create-a-new-expo-app">How to Create A New Expo App</a></li>
<li><a class="post-section-overview" href="#heading-how-to-install-nativewind">How to Install NativeWind</a></li>
<li><a class="post-section-overview" href="#heading-how-to-set-up-tailwind-css">How to Set Up Tailwind CSS</a></li>
<li><a class="post-section-overview" href="#heading-how-to-configure-nativewind-with-babel">How to Configure NativeWind With Babel</a></li>
<li><a class="post-section-overview" href="#heading-how-to-style-with-nativewind">How to Style with NativeWind</a></li>
<li><a class="post-section-overview" href="#heading-how-to-build-a-simple-login-screen">How to Build A Simple Login Screen</a></li>
<li><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></li>
</ul>
<h2 id="heading-whats-nativewind">What's NativeWind?</h2>
<p>NativeWind acts as a bridge between Tailwind CSS and React Native Expo, allowing developers to take advantage of Tailwind's utility-first approach in their mobile app development workflow. </p>
<p>NativeWind provides various benefits to developers, some of these benefits include:</p>
<ul>
<li><strong>Familiar Syntax</strong>: Developers that are familiar with Tailwind CSS can easily migrate to using NativeWind in their React Native projects, easing the learning curve.</li>
<li><strong>Consistent Styling:</strong> NativeWind ensures consistent styling across platforms by offering a single collection of components and services.</li>
<li><strong>Flexibility</strong>: NativeWind allows developers to easily adapt and extend styles to meet the app's design specifications.</li>
</ul>
<p>Overall, It provides a collection of components and tools that are very similar to Tailwind CSS, allowing developers to create shorter, more concise code while preserving flexibility and consistency across platforms.</p>
<blockquote>
<p>Tailwind makes writing code feel like I’m using a design tool - Didier Catz</p>
</blockquote>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li>Basic Understanding of React Native Expo and Tailwind CSS.</li>
<li>Node.js and npm (or yarn) installed.</li>
<li>Willingness to learn :)</li>
</ul>
<h2 id="heading-getting-started">Getting Started</h2>
<p>Before you dive into integrating Tailwind CSS into your React Native Expo app, you'll need to ensure that you have the necessary tools set up.</p>
<p>If you haven't already installed Expo and <a target="_blank" href="https://www.npmjs.com/package/expo-cli">expo-cli</a> globally, you can do so using npm or yarn:</p>
<pre><code class="lang-bash">npm install -g expo-cli
</code></pre>
<p>or </p>
<pre><code class="lang-bash">yarn global add expo-cli
</code></pre>
<h2 id="heading-how-to-create-a-new-expo-app">How to Create A New Expo App</h2>
<p>With expo-cli installed, you can now create a new React Native Expo project. </p>
<p>Navigate to the directory where you wish to create your project and open the terminal. You can do this by pressing <em>CTRL + `</em> on Visual Studio Code. Then execute this command in the terminal:</p>
<pre><code class="lang-bash">npx create-expo-app simpleproject
</code></pre>
<p>This command creates an expo project in your directory.</p>
<h2 id="heading-how-to-install-nativewind">How to Install NativeWind</h2>
<p>After creating your expo project, you can install NativeWind and its dependencies by running the following commands in your project's directory:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> simpleproject
npm i nativewind
npm i --dev tailwindcss@3.3.2
</code></pre>
<p>or</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> simpleproject
yarn add nativewind
yarn add --dev tailwindcss@3.3.2
</code></pre>
<p>Next, you'll need to create a <code>tailwind.config.js</code> file. To do this, run this command in your terminal:</p>
<pre><code class="lang-bash">npx tailwindcss init
</code></pre>
<p>This would result in a <code>tailwind.config.js</code> file in your project's root directory. </p>
<h2 id="heading-how-to-set-up-tailwind-css">How to Set Up Tailwind CSS</h2>
<p>To set up Tailwind CSS in your project, navigate to your <code>tailwind.config.js</code> file, and under <code>content</code>, enter the paths to your components. Your <code>tailwind.config.js</code> file would then look like this:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/** <span class="hljs-doctag">@type <span class="hljs-type">{import('tailwindcss').Config}</span> </span>*/</span>
<span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">content</span>: [
    <span class="hljs-string">"./App.{js,jsx,ts,tsx}"</span>,
    <span class="hljs-string">"./&lt;custom directory&gt;/**/*.{js,jsx,ts,tsx}"</span>,
  ],
  <span class="hljs-attr">theme</span>: {
    <span class="hljs-attr">extend</span>: {},
  },
  <span class="hljs-attr">plugins</span>: [],
};
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/code-1.png" alt="A screenshot of tailwind.conf.js file after adding path to components" width="600" height="400" loading="lazy">
<em>tailwind.conf.js file after adding paths to components</em></p>
<p>In the above example, you can replace <code>&lt;custom directory&gt;</code> with your directory's real name.</p>
<h2 id="heading-how-to-configure-nativewind-with-babel">How to Configure NativeWind with Babel</h2>
<p>You'll also need to configure NativeWind with Babel. To do this, include the NativeWind plugin in your project's <code>babel.conf.js</code> file:</p>
<pre><code class="lang-javascript">plugins: [<span class="hljs-string">"nativewind/babel"</span>],
</code></pre>
<p>The <code>babel.conf.js</code> file would look like this after adding the NativeWind plugin:</p>
<pre><code class="lang-js"><span class="hljs-built_in">module</span>.exports = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">api</span>) </span>{
  api.cache(<span class="hljs-literal">true</span>);
  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">presets</span>: [<span class="hljs-string">"babel-preset-expo"</span>],
    <span class="hljs-attr">plugins</span>: [<span class="hljs-string">"nativewind/babel"</span>],
  };
};
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/code2-2.png" alt="babel.conf.js file after adding nativewind plugin" width="600" height="400" loading="lazy">
<em>babel.conf.js file after adding nativewind plugin</em></p>
<p>By including the NativeWind plugin in the Babel configuration file you ensure that NativeWind's functionality is properly incorporated into your project's JavaScript codebase.</p>
<p> 🎉With this, NativeWind has been successfully integrated into your Expo app. The next step is to begin styling the app with NativeWind.</p>
<h2 id="heading-how-to-style-with-nativewind">How to Style with NativeWind</h2>
<p>To begin styling with NativeWind, go to your project's <code>App.js</code> file or the component you would like to style, which would look like this by default:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { StatusBar } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-status-bar"</span>;
<span class="hljs-keyword">import</span> { StyleSheet, Text, View } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{styles.container}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Text</span>&gt;</span>Open up App.js to start working on your app!<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">StatusBar</span> <span class="hljs-attr">style</span>=<span class="hljs-string">'auto'</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span></span>
  );
}

<span class="hljs-keyword">const</span> styles = StyleSheet.create({
  <span class="hljs-attr">container</span>: {
    <span class="hljs-attr">flex</span>: <span class="hljs-number">1</span>,
    <span class="hljs-attr">backgroundColor</span>: <span class="hljs-string">"#fff"</span>,
    <span class="hljs-attr">alignItems</span>: <span class="hljs-string">"center"</span>,
    <span class="hljs-attr">justifyContent</span>: <span class="hljs-string">"center"</span>,
  },
});
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/appjs-default.png" alt="App.js boilerplate code" width="600" height="400" loading="lazy">
<em>App.js boilerplate code</em></p>
<p>Next, modify your component to get rid of any instances of <code>StyleSheet</code> abstraction. In this example, we'll modify the <code>App.js</code> code. After adjustments, we should have something like this:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { StatusBar } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-status-bar"</span>;
<span class="hljs-comment">// import { StyleSheet, Text, View } from "react-native";</span>
<span class="hljs-keyword">import</span> { Text, View } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="hljs-comment">// &lt;View style={styles.container}&gt;</span>
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'flex-1 justify-center items-center bg-white'</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Text</span>&gt;</span>Open up App.js to start working on your app!<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">StatusBar</span> <span class="hljs-attr">style</span>=<span class="hljs-string">'auto'</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span></span>
  );
}

<span class="hljs-comment">// const styles = StyleSheet.create({</span>
<span class="hljs-comment">//   container: {</span>
<span class="hljs-comment">//     flex: 1,</span>
<span class="hljs-comment">//     backgroundColor: "#fff",</span>
<span class="hljs-comment">//     alignItems: "center",</span>
<span class="hljs-comment">//     justifyContent: "center",</span>
<span class="hljs-comment">//   },</span>
<span class="hljs-comment">// });</span>
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/newww.png" alt="App.js component after modification " width="600" height="400" loading="lazy">
<em>App.js component after modification</em></p>
<p>In the modified codeblock, we remove all occurrences of <code>StyleSheet</code> abstractions, including the import statement for <code>stylesheet</code> and the <code>StyleSheet.create</code> function, and we replace <code>style</code> with <code>className</code> in the <code>App.js</code> return function.</p>
<p>Having cleared that up, all you need to do is write Tailwind CSS classes into your app <code>className</code> to begin implementing Tailwind CSS in your application. You'll see this in a bit as we build a simple login screen with NativeWind.</p>
<h2 id="heading-how-to-build-a-simple-login-screen">How to Build A Simple Login Screen</h2>
<p>Now, let's dive into building a simple login screen using NativeWind. We'll continue with the initial setup in the <code>App.js</code> file and gradually add components to create the login UI.</p>
<p>First, let's replace the existing code in the <code>App.js</code> file with the following:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { StatusBar } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-status-bar"</span>;
<span class="hljs-keyword">import</span> { Text, View } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'flex-1 justify-center items-center bg-white'</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">StatusBar</span> <span class="hljs-attr">style</span>=<span class="hljs-string">'auto'</span> /&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Text</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'text-center mt-3 text-2xl font-light text-orange-300'</span>&gt;</span>
        Login
      <span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
      {/* Additional components goes here */}
    <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span></span>
 );
}
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/firstt.png" alt="Starter code for login screen UI" width="600" height="400" loading="lazy">
<em>Starter code for login screen UI</em></p>
<p>The code above imports the essential components from React Native and Expo. We then use a <code>View</code> component to define the structure of our login screen, which is styled with NativeWind's utility classes. Inside the <code>View</code>, we have a <code>Text</code> component that displays "Login" with styling applied using NativeWind classes.</p>
<p>Next, you can add your login form components, such as username and password input fields, a login button, and any other necessary elements. Here is an example of how you can extend the login screen:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { StatusBar } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-status-bar"</span>;
<span class="hljs-keyword">import</span> { Text, View, TouchableOpacity, TextInput } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'flex-1 justify-center items-center bg-white'</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">StatusBar</span> <span class="hljs-attr">style</span>=<span class="hljs-string">'auto'</span> /&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Text</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'text-center mt-3 text-2xl font-light text-orange-300'</span>&gt;</span>
        Login
      <span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
      {/* Additional components goes here */}
      <span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'mt-5 mx-5'</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">View</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">Text</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'text-gray-400'</span>&gt;</span>EMAIL:<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">TextInput</span>
            <span class="hljs-attr">placeholder</span>=<span class="hljs-string">'Enter Email...'</span>
            <span class="hljs-attr">className</span>=<span class="hljs-string">'border border-dotted p-2 text-gray-500 border-amber-400 mt-1'</span>
          /&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'mt-3'</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">Text</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'text-gray-400'</span>&gt;</span>PASSWORD:<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">TextInput</span>
            <span class="hljs-attr">secureTextEntry</span>
            <span class="hljs-attr">placeholder</span>=<span class="hljs-string">'Enter Password...'</span>
            <span class="hljs-attr">className</span>=<span class="hljs-string">'border text-gray-500 border-dotted p-2 border-amber-400 mt-1'</span>
          /&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">TouchableOpacity</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'bg-orange-300 p-3 mt-4'</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">Text</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'text-center text-base text-white'</span>&gt;</span>Login<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">TouchableOpacity</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">Text</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'text-center font-normal text-gray-500 text-base mt-3'</span>&gt;</span>
          OR
        <span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'mt-4'</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">TouchableOpacity</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'flex flex-row items-center justify-center p-2 bg-orange-300'</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">Text</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'text-white mx-2 text-sm'</span>&gt;</span>Sign In With Google<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">TouchableOpacity</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'mt-6 flex-row justify-center'</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">Text</span> <span class="hljs-attr">className</span>=<span class="hljs-string">''</span>&gt;</span>New to FreeCodeCamp? <span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">TouchableOpacity</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">Text</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'text-amber-500'</span>&gt;</span>Create an Account<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">TouchableOpacity</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span></span>
  );
}
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/nextend-1.png" alt="Extended login screen UI with additional components " width="600" height="400" loading="lazy">
<em>Extended login screen UI with additional components</em></p>
<p>In this expanded version, we've included <code>TextInput</code> components for the username and password input fields, as well as a <code>TouchableOpacity</code> for the login button. Styling is done with NativeWind's utility classes to provide a clean and consistent appearance.</p>
<p>Furthermore, once you've finished creating your login screen using NativeWind in your React Native Expo project, you'll want to test it to check if everything works properly. You can do this by running this command on your terminal:</p>
<pre><code class="lang-bash">expo start
</code></pre>
<p>This command will launch the bundler and generate a QR code. To open the app, scan the QR code displayed in the terminal with your emulator's camera, or press "a" for Android or "i" for iOS.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot_20240223-015329.png" alt="Output of the code in an emulator" width="600" height="400" loading="lazy">
<em>Output of the code in an emulator</em></p>
<p>If you need to, you can access <a target="_blank" href="https://github.com/thejohncaleb/simpleproject">the complete project code</a> on GitHub.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Integrating Tailwind CSS into a React Native Expo project with NativeWind has various benefits, including increased developer efficiency, code consistency, and performance. Developers can easily create amazing mobile applications by leveraging the power of Tailwind CSS's utility-first approach and React Native's native features.</p>
<p>NativeWind makes it easy to apply Tailwind CSS to your React Native Expo app. Using Tailwind CSS in your mobile app development workflow opens up new possibilities for UI design and customization.</p>
<p>Remember, if you have any questions or just want to say hi, feel free to reach me on <a target="_blank" href="https://twitter.com/thejohncaleb">X(Twitter)</a> or my <a target="_blank" href="https://thejohncaleb.netlify.app/contact">website</a>. :)</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Unleash the Power of React Native to Build Mobile Apps ]]>
                </title>
                <description>
                    <![CDATA[ Are you ready to dive into the world of React Native and create stunning mobile applications that work seamlessly on both Android and iOS platforms? We've got you covered! We just published a full React Native course on the freeCodeCamp.org YouTube c... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/react-native-full-course-android-ios-development/</link>
                <guid isPermaLink="false">66b2064ba5be9a107f4341a1</guid>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Mon, 10 Apr 2023 15:37:07 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/04/reactnative.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Are you ready to dive into the world of React Native and create stunning mobile applications that work seamlessly on both Android and iOS platforms? We've got you covered!</p>
<p>We just published a full React Native course on the freeCodeCamp.org YouTube channel. </p>
<p>Emma Williams created this course. She is a software developer who specializes in React Native.</p>
<p>Our comprehensive React Native course is specifically designed to take you from the basics of app development to creating your very own weather app with a sleek user interface and real-time data integration. The course will walk you through core concepts such as components, state management, hooks, and styling, while providing hands-on experience and practical examples.</p>
<h3 id="heading-course-overview">Course Overview</h3>
<p><strong>Introduction</strong><br>Start your journey into React Native with a brief overview of the framework, its benefits, and why it's the go-to choice for cross-platform mobile development.</p>
<p><strong>What is React Native and Expo?</strong><br>Understand the purpose of React Native and learn how Expo simplifies the development process by offering a suite of tools and services.</p>
<p><strong>Setting Up Your Environment</strong><br>Get up and running with Expo, set up a custom app, and configure Android Studio to kickstart your development process.</p>
<p><strong>The Directory Structure, Linting, and Prettier</strong><br>Familiarize yourself with the typical directory structure of a React Native project, and learn how to set up linting and Prettier for a clean and consistent codebase.</p>
<p><strong>Debugging and Native Components</strong><br>Discover the art of debugging in React Native and explore the power of native components to achieve a truly native look and feel.</p>
<p><strong>Core Components and JSX</strong><br>Understand the significance of core components and learn how to use JSX to build dynamic UIs.</p>
<p><strong>Working with Components</strong><br>Dive deep into the world of components, their purpose, and how to create your very first component.</p>
<p><strong>Styling and Layout Props</strong><br>Grasp the essentials of styling in React Native and learn how to use layout props to create responsive designs.</p>
<p><strong>Building the Weather App</strong><br>Put your newfound skills to the test as you create the current weather screen, upcoming weather component, and city screen. Learn to work with lists, images, and props, all while refining your codebase.</p>
<p><strong>Navigation and State Management</strong><br>Delve into the realm of navigation and state management, implementing tabs and using hooks like useState and useEffect to manage your app's state.</p>
<p><strong>Fetching Data and Integrating APIs</strong><br>Learn how to fetch data from the OpenWeatherMap API, get the user's location, and create your own custom hook to pass data to your components.</p>
<p><strong>Updating Components and Installing Moment</strong><br>Update your components to use real-time data, and install Moment to display dates and times in a user-friendly format.</p>
<p><strong>Error Handling and Final Refactoring</strong><br>Create an error screen to handle unexpected issues, and polish your app with some last refactoring.</p>
<p><strong>Bonus Material</strong><br>Enhance your React Native knowledge with bonus content covering advanced topics and best practices.</p>
<p>By the end of this comprehensive React Native course, you'll have the skills and confidence to build your own cross-platform mobile applications with ease. Whether you're an aspiring app developer or a seasoned professional looking to expand your toolkit, this course offers the perfect balance of theory and hands-on practice to elevate your React Native expertise. </p>
<p>Watch the full course on <a target="_blank" href="https://www.youtube.com/watch?v=obH0Po_RdWk">the freeCodeCamp.org YouTube channel</a> (5-hour watch).</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/obH0Po_RdWk" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Manage State in React and React Native with the PullState Library ]]>
                </title>
                <description>
                    <![CDATA[ React and React Native are popular JavaScript libraries that allow developers to create complex user interfaces and mobile applications with ease.  One of the key benefits of both these libraries is that they each have the ability to manage their own... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/state-management-in-react-and-react-native-with-pullstate/</link>
                <guid isPermaLink="false">66bb889e6b3bd8d6bf25ae36</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ State Management  ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Grant Riordan ]]>
                </dc:creator>
                <pubDate>Mon, 03 Apr 2023 21:47:52 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/04/Background1--1-.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>React and React Native are popular JavaScript libraries that allow developers to create complex user interfaces and mobile applications with ease. </p>
<p>One of the key benefits of both these libraries is that they each have the ability to manage their own state within components. But managing state, along with sharable data, can be tricky – especially as the applications become more complex.</p>
<p>In this article, we will explore global state management – and in particular, how to use the Pullstate library. You'll learn how you can easily implement it into your React and React Native projects.</p>
<p>Pre-Requisites:</p>
<ul>
<li>Basic knowledge of React / React Native applications</li>
<li>Basic knowledge of CSS and styling (not essential)</li>
</ul>
<p><strong>Note</strong>: in this article, I will be using TypeScript and React Native. But the implementation and concepts will be exactly the same for JavaScript users and React (without the strongly typed variables).</p>
<h2 id="heading-what-is-state">What is State?</h2>
<p>State refers to an object that stores data which can change over time. It's a way to manage and update data within a component or application without affecting other parts of the application.  </p>
<p>State in React is like a backpack you carry around with you, containing things you might need for your day. It's a way to store and manage data in your app that can change over time, like what you've picked up or dropped off along the way.</p>
<h3 id="heading-local-vs-global-state">Local vs global state</h3>
<p>Component state is local to a specific component. This means that it's only accessible and modifiable within that component. You use component state to manage data that's specific to a single component – for example like form validation messages, or the visibility of certain UI (User Interface) elements.</p>
<p>Global state, on the other hand, refers to data that can be accessed and modified from multiple components across the entire application. Global state would be something that is managed by a state management library. It's useful for managing shared data across multiple components, like authentication status, user preferences, or shopping cart contents.   </p>
<p>Global state and component state both have their own advantages and disadvantages, depending on the specific needs of the application. </p>
<p>Global state can simplify data management and improve performance, but can also increase complexity if not managed correctly. Component state is simpler to manage and understand, but can lead to duplicated data and inconsistent behavior across the application.</p>
<h2 id="heading-what-are-state-management-libraries">What are State Management Libraries?</h2>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/03/SCR-20230325-oif-1.png" alt="Image" width="600" height="400" loading="lazy">
<em>logos of popular state libraries</em></p>
<p>State management libraries in React are tools that help developers manage and organize the state of their applications more easily. These libraries are designed to handle complex state management scenarios in large-scale applications so you don't have to. They also help you create a more central, organized state.   </p>
<p>When talking about state management and data sharing, the most commonly talked about solution is Redux. But Redux is often seen as overly-complicated because it adds an additional layer of complexity. </p>
<p>Redux is designed to manage application state in a predictable and consistent manner, but it requires developers to learn a new set of concepts, such as actions, reducers, and the store. </p>
<p>Some other popular state management libraries in React include Zustand, MobX, and Recoil. These libraries provide features like global state management, immutability, and optimized re-rendering to help make applications more performant and easier to maintain.  </p>
<p>Using a state management library can make it easier for developers to handle the state of their application, and simplify the process of passing data between components. It can take some time to learn how to use these libraries effectively, but they can be very powerful tools for building complex React applications.</p>
<h2 id="heading-what-is-pullstate">What is Pullstate?</h2>
<p>Pullstate is a much simpler and more lightweight state management library for React. It simplifies the whole process and makes it easier to build scalable applications. </p>
<p>Since React Native is built on React, you can use Pullstate to build not only web applications but mobile ones, too. It is is based on the concept of "pulling" data from the state, rather than pushing data into it.</p>
<h3 id="heading-how-to-use-pullstate">How to use Pullstate</h3>
<p>In order to use Pullstate and get up an running, you'll first need to know which package manager you're using (yarn or npm).  </p>
<p>Open your existing React / React Native project, and in the terminal enter the following commands depending on whether you use npm or yarn:</p>
<pre><code class="lang-terminal">npm install pullstate
//or
yarn add pullstate
</code></pre>
<p>If you haven't already got a React Native application you can create one using the React Native cli.  </p>
<p>First, you'll need to make sure you <a target="_blank" href="https://nodejs.org/en/download">have Node.js installed</a>.  </p>
<p>Open the terminal to the folder where you would like to create your React Native application:</p>
<pre><code class="lang-terminal">npm uninstall -g react-native-cli
npx react-native init myReactNativeApp --template react-native-template-typescript
</code></pre>
<h2 id="heading-how-to-set-up-the-store">How to Set Up the Store</h2>
<p>The below code and corresponding image shows how to initialize the Pullstate store code for copying and pasting below (this is how I'll include all the code in this tutorial – code for copying, along with how it should look in a snapshot).  </p>
<p>You can put the below code in its own file, for example <code>store.ts</code> , in your project root folder.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {Store} <span class="hljs-keyword">from</span> <span class="hljs-string">'pullstate'</span>;

<span class="hljs-keyword">interface</span> UIStore {
  user: {
    firstName: <span class="hljs-built_in">string</span>;
    lastName: <span class="hljs-built_in">string</span>;
    acceptedTnC: <span class="hljs-built_in">boolean</span>;
  };
  preferences: {
    isDarkMode: <span class="hljs-built_in">boolean</span>;
    pushNotifications: <span class="hljs-built_in">boolean</span>;
  };
}

<span class="hljs-keyword">const</span> initialStore: UIStore = {
  user: {
    firstName: <span class="hljs-string">''</span>,
    lastName: <span class="hljs-string">''</span>,
    acceptedTnC: <span class="hljs-literal">false</span>,
  },
  preferences: {
    isDarkMode: <span class="hljs-literal">false</span>,
    pushNotifications: <span class="hljs-literal">false</span>,
  },
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> store = <span class="hljs-keyword">new</span> Store&lt;UIStore&gt;({
  user: {
    firstName: <span class="hljs-string">''</span>,
    lastName: <span class="hljs-string">''</span>,
    acceptedTnC: <span class="hljs-literal">false</span>,
  },
  preferences: {
    isDarkMode: <span class="hljs-literal">false</span>,
    pushNotifications: <span class="hljs-literal">false</span>,
  },
});
</code></pre>
<p>Here's the snapshot of what that should look like:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/03/store-2.png" alt="Image" width="600" height="400" loading="lazy">
<em>image showing code snapshot of store configuration</em></p>
<h3 id="heading-what-is-this-code-doing">What is this code doing?</h3>
<p>First, we created an interface for the Pullstate store. This holds all the information we will need to store in state across the app. We have things like the user's first name and last name (so we can use them across the application). </p>
<p>The store can also hold nested objects, for example our <em>preferences</em> object, making managing state cleaner.  </p>
<p>To keep things simple and allow cleaner code in the future, I've created an initial store state. This resets all the state properties to their default values.<br>We can then (when needed) return the state back to its initial state quick and easily. This could be very useful in situations like <strong>logOut</strong>, <strong>onError</strong>, and so on.</p>
<p>Then we initialize the store and make it an exportable object. This means that after the first time we import the object (ideally at the top level of our application) it is then accessible throughout our app.</p>
<h2 id="heading-how-to-retrieve-data-from-the-store">How to Retrieve Data from the Store</h2>
<p>Ok, so we have our store, but how we do access this data within our component?<br>To retrieve the data from the store, we can utilize either the <strong>getRawState</strong> or  <strong>useState</strong> functions.</p>
<p><code>getRawState()</code> returns the raw state at this moment in time. When called it gives the live state of the pull state store. If the state is updated, the value will not be updated</p>
<p>For example in the code below, the <strong>acceptedTerms</strong> variable would be whatever value it was at time of calling <code>store.getRawState()</code>:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/03/getRawState-1.png" alt="Image" width="600" height="400" loading="lazy">
<em>example of using getRawState() function</em></p>
<p><code>useState()</code> acts in the same way as the React <code>useState()</code> function works. When the Pullstate store is updated, the acceptedTerms value will be be updated too, thus causing a re-render of the component. </p>
<p>Think of useState in Pullstate as a listening function – it waits for a value to be updated and then provides you with that new updated value.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/03/usestate.png" alt="Image" width="600" height="400" loading="lazy">
<em>example of using the store.useState() function</em></p>
<h2 id="heading-real-life-example">Real Life Example:</h2>
<p>Let's take a look at a real life example, involving accepting a terms and conditions page. The code below will:</p>
<ul>
<li>Create a card component with some terms, and </li>
<li>Have two buttons, 'Back' and 'Next', which will run a function on clicking.</li>
</ul>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { StyleSheet, Text, View } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> { Card } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native-paper"</span>;
<span class="hljs-keyword">import</span> { store } <span class="hljs-keyword">from</span> <span class="hljs-string">"./store"</span>;<span class="hljs-keyword">import</span> { BlueButton } <span class="hljs-keyword">from</span> <span class="hljs-string">"../atoms/button"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> PullStateCard: React.FC = <span class="hljs-function">() =&gt;</span> {  
    <span class="hljs-keyword">const</span> handleAgreeTerms = <span class="hljs-function">() =&gt;</span> {
        store.update(<span class="hljs-function">(<span class="hljs-params">state</span>) =&gt;</span> {     
            state.user.acceptedTnC = <span class="hljs-literal">true</span>;    
        });  
    };  

    <span class="hljs-keyword">const</span> handleDisagreeTerms = <span class="hljs-function">() =&gt;</span> { 
        store.update(<span class="hljs-function">(<span class="hljs-params">state</span>) =&gt;</span> {      
            state.user.acceptedTnC = <span class="hljs-literal">false</span>;    
        });  
    };  

    <span class="hljs-keyword">return</span> (    
        &lt;Card style={[styles.marginTop, styles.padding]}&gt;
            &lt;Text&gt;Our services are provided on an <span class="hljs-keyword">as</span>-is basis. 
            We <span class="hljs-keyword">do</span> not guarantee the availability, accuracy, or completeness <span class="hljs-keyword">of</span> our services. 
            You may use our services only <span class="hljs-keyword">for</span> lawful purposes and <span class="hljs-keyword">in</span> accordance <span class="hljs-keyword">with</span> these terms and conditions.
            &lt;/Text&gt;
            &lt;Text style={{ marginTop: <span class="hljs-number">16</span> }}&gt;
                By clicking <span class="hljs-string">'Next'</span> you are agreeing to the terms and conditions <span class="hljs-keyword">of</span> <span class="hljs-built_in">this</span> app
            &lt;/Text&gt;
            &lt;View style={styles.row}&gt;
                &lt;BlueButton 
                    styleOverride={styles.button}
                    onPress={handleDisagreeTerms}
                    title=<span class="hljs-string">"Back"</span>/&gt;        
                &lt;BlueButton
                    styleOverride={styles.button}
                    onPress={handleAgreeTerms}
                    title=<span class="hljs-string">"Next"</span>/&gt;
            &lt;/View&gt;
        &lt;/Card&gt;  

       )
}
</code></pre>
<p>My custom buttons:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {
  StyleProp,
  StyleSheet,
  Text,
  TouchableOpacity,
  ViewStyle,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native'</span>;
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">interface</span> BlueButtonProps {
  onPress: <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">void</span>;
  title: <span class="hljs-built_in">string</span>;
  styleOverride?: StyleProp&lt;ViewStyle&gt;;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> BlueButton: React.FC&lt;BlueButtonProps&gt; = <span class="hljs-function">(<span class="hljs-params">{
  onPress,
  title,
  styleOverride,
}</span>) =&gt;</span> (
  &lt;TouchableOpacity onPress={onPress} style={[styles.button, styleOverride]}&gt;
    &lt;Text style={styles.buttonText}&gt;{title}&lt;/Text&gt;
  &lt;/TouchableOpacity&gt;
);

<span class="hljs-keyword">const</span> styles = StyleSheet.create({
  button: {
    backgroundColor: <span class="hljs-string">'#007AFF'</span>,
    borderRadius: <span class="hljs-number">10</span>,
    padding: <span class="hljs-number">10</span>,
    alignItems: <span class="hljs-string">'center'</span>,
    marginVertical: <span class="hljs-number">10</span>,
  },
  buttonText: {
    color: <span class="hljs-string">'white'</span>,
    fontSize: <span class="hljs-number">16</span>,
    fontWeight: <span class="hljs-string">'bold'</span>,
  },
});
</code></pre>
<p>The Card component code would look like this:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/03/CARD-1.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h3 id="heading-dissecting-the-handle-functions">Dissecting the handle functions</h3>
<p>The handle functions are called when the buttons are clicked. Depending on which you click, a particular action will be carried out. But the underlying intent is the same, in that they both modify the state of the pull state store we created earlier.</p>
<p>One will set the <code>user.acceptedTnC</code> to <strong>true</strong>, and the other to <strong>false</strong>. We've used the <code>.update()</code>function on the <code>store</code> object we created earlier, and passed it an anonymous function to update the properties.   </p>
<p>The method is not limited to update just the one property, and you can update multiple properties at once, for example:</p>
<pre><code class="lang-typescript">store.update(<span class="hljs-function">(<span class="hljs-params">state</span>) =&gt;</span> {  
    state.user.acceptedTnC = <span class="hljs-literal">true</span>;  
    state.user.firstName = <span class="hljs-string">"John"</span>;  
    state.user.lastName = <span class="hljs-string">"Doe"</span>;  
    state.preferences.pushNotifications = <span class="hljs-literal">true</span>;
});
</code></pre>
<h2 id="heading-why-is-pullstate-and-global-state-management-so-useful">Why is Pullstate and Global State Management So Useful?</h2>
<p>The beauty of using state in this way means that the component doesn't handle the store, the application does. You can then share this across the whole app, and have other components listen to these changes.  </p>
<p>Let's take the example above one step further and have a separate component that will show a relevant message that's dependent on the state of the acceptedTnC property.</p>
<pre><code><span class="hljs-keyword">const</span> styles = StyleSheet.create({  
    <span class="hljs-attr">disagree</span>: {    
        <span class="hljs-attr">color</span>: <span class="hljs-string">"red"</span>,
        <span class="hljs-attr">textDecoration</span>: <span class="hljs-string">"italic"</span>,
    },
    <span class="hljs-attr">accepted</span>: {
        <span class="hljs-attr">color</span>: <span class="hljs-string">"#0d8009"</span>,
    },  
    <span class="hljs-attr">marginTop</span>: {
        <span class="hljs-attr">marginTop</span>: <span class="hljs-number">10</span>,
    },
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> AgreedToTerms: React.FC = <span class="hljs-function">() =&gt;</span> {  
    <span class="hljs-keyword">const</span> acceptedTerms = store.useState(<span class="hljs-function">(<span class="hljs-params">state</span>) =&gt;</span> state.user.acceptedTnC);
    <span class="hljs-keyword">return</span> (    
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">View</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{styles.marginTop}</span>&gt;</span> 
            {acceptedTerms ? (
                <span class="hljs-tag">&lt;<span class="hljs-name">Text</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{styles.accepted}</span>&gt;</span>
                    You have successfully accepted the Terms and Conditions                  <span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>) : (
                 <span class="hljs-tag">&lt;<span class="hljs-name">Text</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{styles.disagree}</span>&gt;</span>
                     Please accept the Terms and Conditions by pressing 'Next'        
                     <span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>)
            }
        <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span></span>
    )
};
</code></pre><p><img src="https://www.freecodecamp.org/news/content/images/2023/03/accepted.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>If you import these components and add your own styling to the App.tsx (App.js for JS) like so:</p>
<pre><code class="lang-typescript">&lt;PullStateCard /&gt;
&lt;AgreedToTerms /&gt;
</code></pre>
<p>Your whole App.tsx code will look like this:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/03/app_whole.png" alt="Image" width="600" height="400" loading="lazy"></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> {
  SafeAreaView,
  ScrollView,
  StatusBar,
  StyleSheet,
  View,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native'</span>;

<span class="hljs-keyword">import</span> {AgreedToTerms, PullStateCard} <span class="hljs-keyword">from</span> <span class="hljs-string">'./src/pullstate/pullstate_example'</span>;

<span class="hljs-keyword">const</span> App = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> (
    &lt;SafeAreaView&gt;
      &lt;StatusBar barStyle={<span class="hljs-string">'light-content'</span>} /&gt;
      &lt;ScrollView contentInsetAdjustmentBehavior=<span class="hljs-string">"automatic"</span>&gt;
        &lt;View style={styles.container}&gt;
          &lt;PullStateCard /&gt;
          &lt;AgreedToTerms /&gt;
        &lt;/View&gt;
      &lt;/ScrollView&gt;
    &lt;/SafeAreaView&gt;
  );
};

<span class="hljs-keyword">const</span> styles = StyleSheet.create({
  container: {
    height: <span class="hljs-string">'100%'</span>,
    paddingHorizontal: <span class="hljs-number">16</span>,
    paddingVertical: <span class="hljs-number">16</span>,
  },
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<p>Once you have copied the code, you can add your own styling where necessary and run the app. </p>
<p>Click the <strong>Next</strong> button, and your text will turn green and inform you that you've accepted the the terms.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/03/SCR-20230329-tsq.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>If you then click the <strong>Back</strong> button, you will see the text is red informing you that you need to accept the terms:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/03/SCR-20230329-ts9.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>This is all powered by the global Pullstate store. There is no internal state on the <strong>AgreedToTerms</strong> component. </p>
<h2 id="heading-decoupling-logic">Decoupling Logic</h2>
<p>Decoupling refers to the practice of separating different parts of your code so that they are not tightly dependent on / related to each other.</p>
<p>Think of it like building with LEGOs. When you build with LEGOs, each piece can connect to other pieces, but they can also be easily disconnected and reconnected with other pieces. This makes it easier to change and modify your LEGO build.</p>
<p>Similarly, in programming, when we decouple our code, we make it easier to change and modify without affecting other parts of our code. This is especially important in large applications with many different components or modules.</p>
<p>By moving the 'agreed to terms' acceptance state to Pullstate, it can be added and removed to any component that should need it. It's not tightly coupled with the AgreedToTerms component, and it becomes a re-usable piece of state that is globally updated. For example we could use it elsewhere in the app as an authorization mechanism, to show / hide UI elements, and much more.</p>
<h2 id="heading-round-up">Round up</h2>
<p>I hope you've found this article useful and enjoyed this brief introduction to Pullstate and global state management.</p>
<p>What we've covered:</p>
<ul>
<li>What state management libraries are</li>
<li>An introduction to Pullstate and how to integrate it into your application</li>
<li>The benefits of Pullstate and how it works</li>
</ul>
<p>For any more questions don't hesitate to reach out to me on <a target="_blank" href="http://twitter.com/gweaths">Twitter</a>.  </p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
