<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>React Native Land</title>
        <link>https://reactnative.land/</link>
        <description>A blog about React Native development</description>
        <lastBuildDate>Fri, 13 Nov 2026 22:02:16 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>Feed for Node.js</generator>
        <language>en</language>
        <image>
            <title>React Native Land</title>
            <url>https://reactnative.land/logo.jpg</url>
            <link>https://reactnative.land/</link>
        </image>
        <copyright>© 2026 React Native Land. All wrongs reserved.</copyright>
        <item>
            <title><![CDATA[A fuzzy search in C++ with Nitro Modules]]></title>
            <link>https://reactnative.land/posts/a-fuzzy-search-in-cpp-with-nitro</link>
            <guid isPermaLink="false">https://reactnative.land/posts/a-fuzzy-search-in-cpp-with-nitro.en</guid>
            <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In [the last post](/posts/a-fuzzy-search-in-cpp) we wrote a fuzzy search in C++ as a Turbo Module. The matcher was one file, but around it sat a file of glue per platform: `OnLoad.cpp` and CMake on An]]></description>
            <content:encoded><![CDATA[In [the last post](/posts/a-fuzzy-search-in-cpp) we wrote a fuzzy search in C++ as a Turbo Module. The matcher was one file, but around it sat a file of glue per platform: `OnLoad.cpp` and CMake on Android, an Objective-C++ provider on iOS. And the module held a single list for the whole app.

Let's write the same search with [Nitro Modules](/posts/native-code-with-nitro-modules). Nitro can implement a spec in C++ on both platforms, and its autolinking registers it, so the glue goes away. A hybrid object is an object, too: each list gets a searcher of its own.

## Create the module

```sh
npx nitrogen@latest init react-native-fuzzy
```

Trimmed to what matters here:

```
react-native-fuzzy/
  nitro.json
  src/specs/Fuzzy.nitro.ts
  src/index.ts
  cpp/HybridFuzzy.hpp
  cpp/HybridFuzzy.cpp
  nitrogen/generated/
```

There is no `ios/` or `android/` code of our own: the template builds `cpp/` on both platforms, and `nitro.json` names the C++ class that implements the spec:

```json title="nitro.json"
{
  "cxxNamespace": ["fuzzy"],
  "ios": { "iosModuleName": "NitroFuzzy" },
  "android": {
    "androidNamespace": ["fuzzy"],
    "androidCxxLibName": "NitroFuzzy"
  },
  "autolinking": {
    "Fuzzy": { "cpp": "HybridFuzzy" }
  }
}
```

## The spec

The same two methods as before. `c++` on both platforms tells Nitrogen there is no Swift or Kotlin to generate for:

```ts title="src/specs/Fuzzy.nitro.ts"
import type { HybridObject } from 'react-native-nitro-modules';

export interface Fuzzy extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
  setItems(items: string[]): void;
  search(query: string, limit: number): number[];
}
```

```sh
npx nitrogen
```

Nitrogen writes `HybridFuzzySpec`, a C++ class with a pure virtual method for each one in the spec. The types are plain C++: `string[]` is a `std::vector<std::string>`, and `number` is a `double`. No `jsi::Runtime` in sight.

## C++

The class overrides the spec's methods. Its constructor names the object for Nitro's logs, and `getExternalMemorySize` tells the JavaScript garbage collector how much native memory it holds, so a large list is collected in time once nothing refers to it:

```cpp title="cpp/HybridFuzzy.hpp"
#pragma once

#include "HybridFuzzySpec.hpp"

#include <string>
#include <vector>

namespace margelo::nitro::fuzzy {

class HybridFuzzy : public HybridFuzzySpec {
 public:
  HybridFuzzy() : HybridObject(TAG) {}

  void setItems(const std::vector<std::string>& items) override;
  std::vector<double> search(const std::string& query, double limit) override;

  size_t getExternalMemorySize() noexcept override;

 private:
  std::vector<std::string> items_;
  size_t bytes_ = 0;
};

} // namespace margelo::nitro::fuzzy
```

The matcher is the one from the Turbo Module, word for word. Only the edges change: the arguments arrive as `const` references, and the indexes go back as `double`:

```cpp title="cpp/HybridFuzzy.cpp"
#include "HybridFuzzy.hpp"

#include <algorithm>
#include <utility>

namespace margelo::nitro::fuzzy {

namespace {

// ASCII lower case; other bytes pass through, so UTF-8 still matches itself.
char lower(char c) {
  return c >= 'A' && c <= 'Z' ? static_cast<char>(c + ('a' - 'A')) : c;
}

bool startsWord(const std::string& text, size_t i) {
  if (i == 0) return true;
  char before = text[i - 1];
  return before == ' ' || before == '-' || before == '_' || before == '/';
}

// The query's letters in the item, in order: a score, or -1 when one is missing.
int score(const std::string& query, const std::string& item) {
  int total = 0;
  int streak = 0;
  size_t found = 0;
  for (size_t i = 0; i < item.size() && found < query.size(); i++) {
    if (lower(item[i]) != query[found]) {
      streak = 0;
      continue;
    }
    streak++;
    total += 1 + 2 * streak + (startsWord(item, i) ? 8 : 0);
    found++;
  }
  if (found < query.size()) return -1;
  return total - static_cast<int>(item.size() / 8);
}

} // namespace

void HybridFuzzy::setItems(const std::vector<std::string>& items) {
  items_ = items;
  bytes_ = 0;
  for (const auto& item : items_) bytes_ += sizeof(std::string) + item.capacity();
}

std::vector<double> HybridFuzzy::search(const std::string& query, double limit) {
  std::string q;
  q.reserve(query.size());
  for (char c : query) {
    if (c != ' ') q.push_back(lower(c));
  }

  // (score, index) for every item that has all the letters.
  std::vector<std::pair<int, int>> hits;
  hits.reserve(items_.size());
  for (int i = 0; i < static_cast<int>(items_.size()); i++) {
    int s = score(q, items_[i]);
    if (s >= 0) hits.emplace_back(s, i);
  }

  // Only the best `limit` are sorted: a short screen of a long list costs little.
  auto count = std::min(hits.size(), static_cast<size_t>(std::max(limit, 0.0)));
  std::partial_sort(hits.begin(), hits.begin() + count, hits.end(), [](const auto& a, const auto& b) {
    return a.first != b.first ? a.first > b.first : a.second < b.second;
  });

  std::vector<double> best;
  best.reserve(count);
  for (size_t k = 0; k < count; k++) best.push_back(static_cast<double>(hits[k].second));
  return best;
}

size_t HybridFuzzy::getExternalMemorySize() noexcept {
  return bytes_;
}

} // namespace margelo::nitro::fuzzy
```

Calls still run on the JavaScript thread, synchronously, so `items_` needs no lock. For a list so long that one search takes more than a frame, the spec could return a `Promise<number[]>` instead, and the C++ run it on another thread.

## TypeScript

The module's entry hands out a new searcher on each call:

```ts title="src/index.ts"
import { NitroModules } from 'react-native-nitro-modules';
import type { Fuzzy } from './specs/Fuzzy.nitro';

export type { Fuzzy };

export function createFuzzy(): Fuzzy {
  return NitroModules.createHybridObject<Fuzzy>('Fuzzy');
}
```

So the hook keeps one per component. Two screens searching two lists no longer share anything, and a searcher goes away with its component:

```ts title="useFuzzy.ts"
import { useMemo, useState } from 'react';
import { createFuzzy } from 'react-native-fuzzy';

type Item = { id: string; title: string };

export function useFuzzy(items: Item[], query: string, limit = 50): Item[] {
  const [fuzzy] = useState(createFuzzy);

  // setItems only replaces the list, so running it again for the same items is harmless.
  const loaded = useMemo(() => {
    fuzzy.setItems(items.map((item) => item.title));
    return items;
  }, [fuzzy, items]);

  return useMemo(
    () => (query.trim() ? fuzzy.search(query, limit).map((i) => loaded[i]) : loaded),
    [fuzzy, loaded, query, limit],
  );
}
```

`SearchScreen` does not change: `useFuzzy(items, state.query)` is the same call as with the Turbo Module.

## Run it

```sh
npm install react-native-nitro-modules ./react-native-fuzzy
cd ios && bundle exec pod install && cd ..
npx react-native run-ios
npx react-native run-android
```

## Turbo or Nitro, for C++

- **Turbo Modules**: nothing beyond React Native. In return, each platform needs its glue, the module is one per app, and its methods take the `jsi::Runtime`.
- **Nitro Modules**: the same C++ with no glue, plain C++ types, an object per list, and a way to tell the garbage collector what it holds. In return, a dependency and a generator to run yourself.

The matcher did not change at all between the two. That is the point of writing it in C++: the logic stays, and only the edges move. The [Nitro docs](https://nitro.margelo.com/) cover more C++ types, including callbacks and `Promise`.]]></content:encoded>
            <author>rick@reactnative.land (React Native Land)</author>
        </item>
        <item>
            <title><![CDATA[Uma busca fuzzy em C++ com Nitro Modules]]></title>
            <link>https://reactnative.land/pt/posts/a-fuzzy-search-in-cpp-with-nitro</link>
            <guid isPermaLink="false">https://reactnative.land/posts/a-fuzzy-search-in-cpp-with-nitro.pt-br</guid>
            <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[No [post anterior](/pt/posts/a-fuzzy-search-in-cpp) escrevemos uma busca fuzzy em C++ como um Turbo Module. O matcher era um arquivo só, mas em volta dele havia cola em cada plataforma: `OnLoad.cpp` e]]></description>
            <content:encoded><![CDATA[No [post anterior](/pt/posts/a-fuzzy-search-in-cpp) escrevemos uma busca fuzzy em C++ como um Turbo Module. O matcher era um arquivo só, mas em volta dele havia cola em cada plataforma: `OnLoad.cpp` e CMake no Android, um provider em Objective-C++ no iOS. E o módulo guardava uma única lista para o app inteiro.

Vamos escrever a mesma busca com [Nitro Modules](/pt/posts/native-code-with-nitro-modules). O Nitro consegue implementar uma spec em C++ nas duas plataformas, e o autolinking dele a registra, então a cola desaparece. E um hybrid object é um objeto: cada lista ganha um buscador próprio.

## Crie o módulo

```sh
npx nitrogen@latest init react-native-fuzzy
```

Reduzido ao que importa aqui:

```
react-native-fuzzy/
  nitro.json
  src/specs/Fuzzy.nitro.ts
  src/index.ts
  cpp/HybridFuzzy.hpp
  cpp/HybridFuzzy.cpp
  nitrogen/generated/
```

Não há código nosso em `ios/` nem em `android/`: o template compila `cpp/` nas duas plataformas, e o `nitro.json` dá o nome da classe C++ que implementa a spec:

```json title="nitro.json"
{
  "cxxNamespace": ["fuzzy"],
  "ios": { "iosModuleName": "NitroFuzzy" },
  "android": {
    "androidNamespace": ["fuzzy"],
    "androidCxxLibName": "NitroFuzzy"
  },
  "autolinking": {
    "Fuzzy": { "cpp": "HybridFuzzy" }
  }
}
```

## A spec

Os mesmos dois métodos de antes. `c++` nas duas plataformas diz ao Nitrogen que não há Swift nem Kotlin para gerar:

```ts title="src/specs/Fuzzy.nitro.ts"
import type { HybridObject } from 'react-native-nitro-modules';

export interface Fuzzy extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
  setItems(items: string[]): void;
  search(query: string, limit: number): number[];
}
```

```sh
npx nitrogen
```

O Nitrogen escreve o `HybridFuzzySpec`, uma classe C++ com um método virtual puro para cada um da spec. Os tipos são C++ comuns: `string[]` é um `std::vector<std::string>`, e `number` é um `double`. Nenhum `jsi::Runtime` à vista.

## C++

A classe sobrescreve os métodos da spec. O construtor dá nome ao objeto nos logs do Nitro, e `getExternalMemorySize` diz ao garbage collector do JavaScript quanta memória nativa ele segura, para que uma lista grande seja coletada a tempo quando ninguém mais a referencia:

```cpp title="cpp/HybridFuzzy.hpp"
#pragma once

#include "HybridFuzzySpec.hpp"

#include <string>
#include <vector>

namespace margelo::nitro::fuzzy {

class HybridFuzzy : public HybridFuzzySpec {
 public:
  HybridFuzzy() : HybridObject(TAG) {}

  void setItems(const std::vector<std::string>& items) override;
  std::vector<double> search(const std::string& query, double limit) override;

  size_t getExternalMemorySize() noexcept override;

 private:
  std::vector<std::string> items_;
  size_t bytes_ = 0;
};

} // namespace margelo::nitro::fuzzy
```

O matcher é o do Turbo Module, palavra por palavra. Só as bordas mudam: os argumentos chegam como referências `const`, e os índices voltam como `double`:

```cpp title="cpp/HybridFuzzy.cpp"
#include "HybridFuzzy.hpp"

#include <algorithm>
#include <utility>

namespace margelo::nitro::fuzzy {

namespace {

// ASCII lower case; other bytes pass through, so UTF-8 still matches itself.
char lower(char c) {
  return c >= 'A' && c <= 'Z' ? static_cast<char>(c + ('a' - 'A')) : c;
}

bool startsWord(const std::string& text, size_t i) {
  if (i == 0) return true;
  char before = text[i - 1];
  return before == ' ' || before == '-' || before == '_' || before == '/';
}

// The query's letters in the item, in order: a score, or -1 when one is missing.
int score(const std::string& query, const std::string& item) {
  int total = 0;
  int streak = 0;
  size_t found = 0;
  for (size_t i = 0; i < item.size() && found < query.size(); i++) {
    if (lower(item[i]) != query[found]) {
      streak = 0;
      continue;
    }
    streak++;
    total += 1 + 2 * streak + (startsWord(item, i) ? 8 : 0);
    found++;
  }
  if (found < query.size()) return -1;
  return total - static_cast<int>(item.size() / 8);
}

} // namespace

void HybridFuzzy::setItems(const std::vector<std::string>& items) {
  items_ = items;
  bytes_ = 0;
  for (const auto& item : items_) bytes_ += sizeof(std::string) + item.capacity();
}

std::vector<double> HybridFuzzy::search(const std::string& query, double limit) {
  std::string q;
  q.reserve(query.size());
  for (char c : query) {
    if (c != ' ') q.push_back(lower(c));
  }

  // (score, index) for every item that has all the letters.
  std::vector<std::pair<int, int>> hits;
  hits.reserve(items_.size());
  for (int i = 0; i < static_cast<int>(items_.size()); i++) {
    int s = score(q, items_[i]);
    if (s >= 0) hits.emplace_back(s, i);
  }

  // Only the best `limit` are sorted: a short screen of a long list costs little.
  auto count = std::min(hits.size(), static_cast<size_t>(std::max(limit, 0.0)));
  std::partial_sort(hits.begin(), hits.begin() + count, hits.end(), [](const auto& a, const auto& b) {
    return a.first != b.first ? a.first > b.first : a.second < b.second;
  });

  std::vector<double> best;
  best.reserve(count);
  for (size_t k = 0; k < count; k++) best.push_back(static_cast<double>(hits[k].second));
  return best;
}

size_t HybridFuzzy::getExternalMemorySize() noexcept {
  return bytes_;
}

} // namespace margelo::nitro::fuzzy
```

As chamadas continuam rodando na thread do JavaScript, de forma síncrona, então `items_` não precisa de lock. Para uma lista tão longa que uma busca leve mais de um quadro, a spec poderia devolver uma `Promise<number[]>`, e o C++ rodá-la em outra thread.

## TypeScript

A entrada do módulo entrega um buscador novo a cada chamada:

```ts title="src/index.ts"
import { NitroModules } from 'react-native-nitro-modules';
import type { Fuzzy } from './specs/Fuzzy.nitro';

export type { Fuzzy };

export function createFuzzy(): Fuzzy {
  return NitroModules.createHybridObject<Fuzzy>('Fuzzy');
}
```

Então o hook guarda um por componente. Duas telas buscando em duas listas não compartilham mais nada, e um buscador vai embora junto com o componente:

```ts title="useFuzzy.ts"
import { useMemo, useState } from 'react';
import { createFuzzy } from 'react-native-fuzzy';

type Item = { id: string; title: string };

export function useFuzzy(items: Item[], query: string, limit = 50): Item[] {
  const [fuzzy] = useState(createFuzzy);

  // setItems só substitui a lista, então rodá-lo de novo com os mesmos itens não faz mal.
  const loaded = useMemo(() => {
    fuzzy.setItems(items.map((item) => item.title));
    return items;
  }, [fuzzy, items]);

  return useMemo(
    () => (query.trim() ? fuzzy.search(query, limit).map((i) => loaded[i]) : loaded),
    [fuzzy, loaded, query, limit],
  );
}
```

O `SearchScreen` não muda: `useFuzzy(items, state.query)` é a mesma chamada que com o Turbo Module.

## Rode

```sh
npm install react-native-nitro-modules ./react-native-fuzzy
cd ios && bundle exec pod install && cd ..
npx react-native run-ios
npx react-native run-android
```

## Turbo ou Nitro, para C++

- **Turbo Modules**: nada além do React Native. Em troca, cada plataforma pede a sua cola, o módulo é um por app, e os métodos recebem o `jsi::Runtime`.
- **Nitro Modules**: o mesmo C++ sem cola, tipos C++ comuns, um objeto por lista e um jeito de dizer ao garbage collector o que ele segura. Em troca, uma dependência e um gerador que você mesmo roda.

O matcher não mudou nada entre os dois. É para isso que se escreve em C++: a lógica fica, e só as bordas se mexem. A [documentação do Nitro](https://nitro.margelo.com/) cobre mais tipos C++, incluindo callbacks e `Promise`.]]></content:encoded>
            <author>rick@reactnative.land (React Native Land)</author>
        </item>
        <item>
            <title><![CDATA[A fuzzy search in C++ with Turbo Modules]]></title>
            <link>https://reactnative.land/posts/a-fuzzy-search-in-cpp</link>
            <guid isPermaLink="false">https://reactnative.land/posts/a-fuzzy-search-in-cpp.en</guid>
            <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In [the searchable list](/posts/a-searchable-list) we filtered with `includes`, and debounced the typing so we wouldn't filter on every keystroke. Let's replace both with a fuzzy search written in C++]]></description>
            <content:encoded><![CDATA[In [the searchable list](/posts/a-searchable-list) we filtered with `includes`, and debounced the typing so we wouldn't filter on every keystroke. Let's replace both with a fuzzy search written in C++, as a C++ Turbo Module: `srchlst` finds "Searchable list", and the results arrive in the same frame as the key.

## Why C++ here

[The battery module](/posts/native-code-with-turbo-modules) was a poor fit for C++: it calls platform APIs, so the code was different on each side anyway. A fuzzy matcher is the opposite:

- It is pure computation: no platform API, so one file serves iOS and Android.
- It is a tight loop over thousands of strings, run on every keystroke. Compiled ahead of time, it allocates nothing per character and gives the garbage collector nothing to do.
- A C++ Turbo Module is called through JSI, synchronously. `search` returns an array, not a promise, so there is no frame to wait.

JavaScript is fast too. Measure on your list and your slowest device first; if `filter` keeps up, keep it.

## The shape

In a React Native app, trimmed to what matters here:

```
specs/NativeFuzzy.ts
package.json
shared/NativeFuzzy.h
shared/NativeFuzzy.cpp
android/app/build.gradle
android/app/src/main/jni/CMakeLists.txt
android/app/src/main/jni/OnLoad.cpp
ios/NativeFuzzyProvider.h
ios/NativeFuzzyProvider.mm
```

## The spec

The list crosses into C++ once, with `setItems`. Each keystroke then sends only the query, and gets back the indexes of the best matches:

```ts title="specs/NativeFuzzy.ts"
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  readonly setItems: (items: string[]) => void;
  readonly search: (query: string, limit: number) => number[];
}

export default TurboModuleRegistry.getEnforcing<Spec>('NativeFuzzy');
```

```json title="package.json"
{
  "codegenConfig": {
    "name": "AppSpecs",
    "type": "modules",
    "jsSrcsDir": "specs",
    "android": { "javaPackageName": "com.fuzzyapp.specs" },
    "ios": { "modulesProvider": { "NativeFuzzy": "NativeFuzzyProvider" } }
  }
}
```

## C++

Codegen writes `NativeFuzzyCxxSpec`, a template the module extends. Its methods take the JavaScript runtime first, then the spec's arguments as C++ types: `string[]` arrives as a `std::vector<std::string>`, and a `std::vector<int>` goes back as an array of numbers:

```cpp title="shared/NativeFuzzy.h"
#pragma once

#include <AppSpecsJSI.h>

#include <memory>
#include <string>
#include <vector>

namespace facebook::react {

class NativeFuzzy : public NativeFuzzyCxxSpec<NativeFuzzy> {
 public:
  explicit NativeFuzzy(std::shared_ptr<CallInvoker> jsInvoker);

  void setItems(jsi::Runtime& rt, std::vector<std::string> items);
  std::vector<int> search(jsi::Runtime& rt, std::string query, int limit);

 private:
  std::vector<std::string> items_;
};

} // namespace facebook::react
```

The matcher walks each item once, looking for the query's letters in order. A letter scores more when it follows the previous match, and more again at the start of a word; a long item loses a little, so "List" beats "A long list of settings":

```cpp title="shared/NativeFuzzy.cpp"
#include "NativeFuzzy.h"

#include <algorithm>
#include <utility>

namespace facebook::react {

namespace {

// ASCII lower case; other bytes pass through, so UTF-8 still matches itself.
char lower(char c) {
  return c >= 'A' && c <= 'Z' ? static_cast<char>(c + ('a' - 'A')) : c;
}

bool startsWord(const std::string& text, size_t i) {
  if (i == 0) return true;
  char before = text[i - 1];
  return before == ' ' || before == '-' || before == '_' || before == '/';
}

// The query's letters in the item, in order: a score, or -1 when one is missing.
int score(const std::string& query, const std::string& item) {
  int total = 0;
  int streak = 0;
  size_t found = 0;
  for (size_t i = 0; i < item.size() && found < query.size(); i++) {
    if (lower(item[i]) != query[found]) {
      streak = 0;
      continue;
    }
    streak++;
    total += 1 + 2 * streak + (startsWord(item, i) ? 8 : 0);
    found++;
  }
  if (found < query.size()) return -1;
  return total - static_cast<int>(item.size() / 8);
}

} // namespace

NativeFuzzy::NativeFuzzy(std::shared_ptr<CallInvoker> jsInvoker)
    : NativeFuzzyCxxSpec(std::move(jsInvoker)) {}

void NativeFuzzy::setItems(jsi::Runtime&, std::vector<std::string> items) {
  items_ = std::move(items);
}

std::vector<int> NativeFuzzy::search(jsi::Runtime&, std::string query, int limit) {
  std::string q;
  q.reserve(query.size());
  for (char c : query) {
    if (c != ' ') q.push_back(lower(c));
  }

  // (score, index) for every item that has all the letters.
  std::vector<std::pair<int, int>> hits;
  hits.reserve(items_.size());
  for (int i = 0; i < static_cast<int>(items_.size()); i++) {
    int s = score(q, items_[i]);
    if (s >= 0) hits.emplace_back(s, i);
  }

  // Only the best `limit` are sorted: a short screen of a long list costs little.
  auto count = std::min(hits.size(), static_cast<size_t>(std::max(limit, 0)));
  std::partial_sort(hits.begin(), hits.begin() + count, hits.end(), [](const auto& a, const auto& b) {
    return a.first != b.first ? a.first > b.first : a.second < b.second;
  });

  std::vector<int> best;
  best.reserve(count);
  for (size_t k = 0; k < count; k++) best.push_back(hits[k].second);
  return best;
}

} // namespace facebook::react
```

The matcher is greedy: it takes each letter's first occurrence, which is fast and good enough for a list of names. Every call runs on the JavaScript thread, so `items_` needs no lock.

## Android

Gradle builds the C++ through CMake. `build.gradle` points at the CMake file:

```groovy title="android/app/build.gradle"
android {
  externalNativeBuild {
    cmake {
      path "src/main/jni/CMakeLists.txt"
    }
  }
}
```

The CMake file adds `shared/` to the library React Native already builds for the app:

```cmake title="android/app/src/main/jni/CMakeLists.txt"
cmake_minimum_required(VERSION 3.13)
project(appmodules)

include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)

target_sources(${CMAKE_PROJECT_NAME} PRIVATE ../../../../../shared/NativeFuzzy.cpp)
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ../../../../../shared)
```

`OnLoad.cpp` is copied from React Native's template for your version. Only its C++ module provider changes, to hand out the module by name:

```cpp title="android/app/src/main/jni/OnLoad.cpp"
#include <NativeFuzzy.h>

std::shared_ptr<TurboModule> cxxModuleProvider(
    const std::string& name,
    const std::shared_ptr<CallInvoker>& jsInvoker) {
  if (name == NativeFuzzy::kModuleName) {
    return std::make_shared<NativeFuzzy>(jsInvoker);
  }
  return autolinking_cxxModuleProvider(name, jsInvoker);
}
```

No Kotlin at all.

## iOS

Xcode compiles `shared/` once the folder is added to the app's target. A small provider, named in `package.json`, creates the module:

```objective-c title="ios/NativeFuzzyProvider.h"
#import <Foundation/Foundation.h>
#import <ReactCommon/RCTTurboModule.h>

NS_ASSUME_NONNULL_BEGIN

@interface NativeFuzzyProvider : NSObject <RCTModuleProvider>
@end

NS_ASSUME_NONNULL_END
```

```objective-cpp title="ios/NativeFuzzyProvider.mm"
#import "NativeFuzzyProvider.h"
#import <ReactCommon/CallInvoker.h>
#import <ReactCommon/TurboModule.h>
#import "NativeFuzzy.h"

@implementation NativeFuzzyProvider

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params {
  return std::make_shared<facebook::react::NativeFuzzy>(params.jsInvoker);
}

@end
```

No Swift either: the two files are glue, and the search is the same C++ on both platforms.

## TypeScript

The hook hands the titles over when the list changes, and searches while rendering. `search` is synchronous, so the debounce from the searchable list can go:

```ts title="useFuzzy.ts"
import { useMemo } from 'react';
import NativeFuzzy from './specs/NativeFuzzy';

type Item = { id: string; title: string };

export function useFuzzy(items: Item[], query: string, limit = 50): Item[] {
  // setItems only replaces the list, so running it again for the same items is harmless.
  const loaded = useMemo(() => {
    NativeFuzzy.setItems(items.map((item) => item.title));
    return items;
  }, [items]);

  return useMemo(
    () => (query.trim() ? NativeFuzzy.search(query, limit).map((i) => loaded[i]) : loaded),
    [loaded, query, limit],
  );
}
```

The module keeps one list. That is all this screen needs; two screens searching at once would want a list per id.

In `SearchScreen`, the `useDebounced` and `useMemo` lines become one:

```tsx title="SearchScreen.tsx"
const visible = useFuzzy(items, state.query);
```

## Run it

Codegen runs at `pod install` and in Gradle, so the C++ spec header exists before the C++ is compiled:

```sh
cd ios && bundle exec pod install && cd ..
npx react-native run-ios
npx react-native run-android
```

C++ earns its place when the work is computation, the same on every platform, and called often enough that a frame matters. For anything that talks to the platform, Swift and Kotlin stay the better tools. The [React Native docs on C++ Turbo Native Modules](https://reactnative.dev/docs/the-new-architecture/pure-cxx-modules) cover more types, including custom ones.]]></content:encoded>
            <author>rick@reactnative.land (React Native Land)</author>
        </item>
        <item>
            <title><![CDATA[Uma busca fuzzy em C++ com Turbo Modules]]></title>
            <link>https://reactnative.land/pt/posts/a-fuzzy-search-in-cpp</link>
            <guid isPermaLink="false">https://reactnative.land/posts/a-fuzzy-search-in-cpp.pt-br</guid>
            <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Na [lista pesquisável](/pt/posts/a-searchable-list) filtramos com `includes` e usamos debounce na digitação para não filtrar a cada tecla. Vamos trocar os dois por uma busca fuzzy escrita em C++, como]]></description>
            <content:encoded><![CDATA[Na [lista pesquisável](/pt/posts/a-searchable-list) filtramos com `includes` e usamos debounce na digitação para não filtrar a cada tecla. Vamos trocar os dois por uma busca fuzzy escrita em C++, como um C++ Turbo Module: `srchlst` encontra "Searchable list", e os resultados chegam no mesmo quadro da tecla.

## Por que C++ aqui

O [módulo de bateria](/pt/posts/native-code-with-turbo-modules) não combinava com C++: ele chama APIs da plataforma, então o código era diferente em cada lado de qualquer jeito. Uma busca fuzzy é o oposto:

- É computação pura: nenhuma API da plataforma, então um só arquivo serve iOS e Android.
- É um laço apertado sobre milhares de strings, rodado a cada tecla. Compilado antes, ele não aloca nada por caractere e não dá trabalho ao garbage collector.
- Um C++ Turbo Module é chamado pelo JSI, de forma síncrona. `search` devolve um array, não uma promise, então não há quadro a esperar.

O JavaScript também é rápido. Meça antes, com a sua lista e o seu aparelho mais lento; se o `filter` dá conta, fique com ele.

## A forma

Num app React Native, reduzido ao que importa aqui:

```
specs/NativeFuzzy.ts
package.json
shared/NativeFuzzy.h
shared/NativeFuzzy.cpp
android/app/build.gradle
android/app/src/main/jni/CMakeLists.txt
android/app/src/main/jni/OnLoad.cpp
ios/NativeFuzzyProvider.h
ios/NativeFuzzyProvider.mm
```

## A spec

A lista passa para o C++ uma vez, com `setItems`. Depois, cada tecla envia só a consulta e recebe de volta os índices dos melhores resultados:

```ts title="specs/NativeFuzzy.ts"
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  readonly setItems: (items: string[]) => void;
  readonly search: (query: string, limit: number) => number[];
}

export default TurboModuleRegistry.getEnforcing<Spec>('NativeFuzzy');
```

```json title="package.json"
{
  "codegenConfig": {
    "name": "AppSpecs",
    "type": "modules",
    "jsSrcsDir": "specs",
    "android": { "javaPackageName": "com.fuzzyapp.specs" },
    "ios": { "modulesProvider": { "NativeFuzzy": "NativeFuzzyProvider" } }
  }
}
```

## C++

O Codegen escreve o `NativeFuzzyCxxSpec`, um template que o módulo estende. Os métodos recebem primeiro o runtime do JavaScript e depois os argumentos da spec como tipos C++: `string[]` chega como `std::vector<std::string>`, e um `std::vector<int>` volta como um array de números:

```cpp title="shared/NativeFuzzy.h"
#pragma once

#include <AppSpecsJSI.h>

#include <memory>
#include <string>
#include <vector>

namespace facebook::react {

class NativeFuzzy : public NativeFuzzyCxxSpec<NativeFuzzy> {
 public:
  explicit NativeFuzzy(std::shared_ptr<CallInvoker> jsInvoker);

  void setItems(jsi::Runtime& rt, std::vector<std::string> items);
  std::vector<int> search(jsi::Runtime& rt, std::string query, int limit);

 private:
  std::vector<std::string> items_;
};

} // namespace facebook::react
```

O matcher percorre cada item uma vez, procurando as letras da consulta em ordem. Uma letra vale mais quando vem logo depois da anterior, e mais ainda no começo de uma palavra; um item longo perde um pouco, então "List" vence "A long list of settings":

```cpp title="shared/NativeFuzzy.cpp"
#include "NativeFuzzy.h"

#include <algorithm>
#include <utility>

namespace facebook::react {

namespace {

// ASCII lower case; other bytes pass through, so UTF-8 still matches itself.
char lower(char c) {
  return c >= 'A' && c <= 'Z' ? static_cast<char>(c + ('a' - 'A')) : c;
}

bool startsWord(const std::string& text, size_t i) {
  if (i == 0) return true;
  char before = text[i - 1];
  return before == ' ' || before == '-' || before == '_' || before == '/';
}

// The query's letters in the item, in order: a score, or -1 when one is missing.
int score(const std::string& query, const std::string& item) {
  int total = 0;
  int streak = 0;
  size_t found = 0;
  for (size_t i = 0; i < item.size() && found < query.size(); i++) {
    if (lower(item[i]) != query[found]) {
      streak = 0;
      continue;
    }
    streak++;
    total += 1 + 2 * streak + (startsWord(item, i) ? 8 : 0);
    found++;
  }
  if (found < query.size()) return -1;
  return total - static_cast<int>(item.size() / 8);
}

} // namespace

NativeFuzzy::NativeFuzzy(std::shared_ptr<CallInvoker> jsInvoker)
    : NativeFuzzyCxxSpec(std::move(jsInvoker)) {}

void NativeFuzzy::setItems(jsi::Runtime&, std::vector<std::string> items) {
  items_ = std::move(items);
}

std::vector<int> NativeFuzzy::search(jsi::Runtime&, std::string query, int limit) {
  std::string q;
  q.reserve(query.size());
  for (char c : query) {
    if (c != ' ') q.push_back(lower(c));
  }

  // (score, index) for every item that has all the letters.
  std::vector<std::pair<int, int>> hits;
  hits.reserve(items_.size());
  for (int i = 0; i < static_cast<int>(items_.size()); i++) {
    int s = score(q, items_[i]);
    if (s >= 0) hits.emplace_back(s, i);
  }

  // Only the best `limit` are sorted: a short screen of a long list costs little.
  auto count = std::min(hits.size(), static_cast<size_t>(std::max(limit, 0)));
  std::partial_sort(hits.begin(), hits.begin() + count, hits.end(), [](const auto& a, const auto& b) {
    return a.first != b.first ? a.first > b.first : a.second < b.second;
  });

  std::vector<int> best;
  best.reserve(count);
  for (size_t k = 0; k < count; k++) best.push_back(hits[k].second);
  return best;
}

} // namespace facebook::react
```

O matcher é guloso: ele pega a primeira ocorrência de cada letra, o que é rápido e bom o bastante para uma lista de nomes. Toda chamada roda na thread do JavaScript, então `items_` não precisa de lock.

## Android

O Gradle compila o C++ pelo CMake. O `build.gradle` aponta para o arquivo do CMake:

```groovy title="android/app/build.gradle"
android {
  externalNativeBuild {
    cmake {
      path "src/main/jni/CMakeLists.txt"
    }
  }
}
```

O arquivo do CMake acrescenta `shared/` à biblioteca que o React Native já compila para o app:

```cmake title="android/app/src/main/jni/CMakeLists.txt"
cmake_minimum_required(VERSION 3.13)
project(appmodules)

include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)

target_sources(${CMAKE_PROJECT_NAME} PRIVATE ../../../../../shared/NativeFuzzy.cpp)
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ../../../../../shared)
```

O `OnLoad.cpp` é copiado do template do React Native da sua versão. Só o provedor de módulos C++ muda, para entregar o módulo pelo nome:

```cpp title="android/app/src/main/jni/OnLoad.cpp"
#include <NativeFuzzy.h>

std::shared_ptr<TurboModule> cxxModuleProvider(
    const std::string& name,
    const std::shared_ptr<CallInvoker>& jsInvoker) {
  if (name == NativeFuzzy::kModuleName) {
    return std::make_shared<NativeFuzzy>(jsInvoker);
  }
  return autolinking_cxxModuleProvider(name, jsInvoker);
}
```

Nada de Kotlin.

## iOS

O Xcode compila `shared/` depois que a pasta é adicionada ao target do app. Um provider pequeno, citado no `package.json`, cria o módulo:

```objective-c title="ios/NativeFuzzyProvider.h"
#import <Foundation/Foundation.h>
#import <ReactCommon/RCTTurboModule.h>

NS_ASSUME_NONNULL_BEGIN

@interface NativeFuzzyProvider : NSObject <RCTModuleProvider>
@end

NS_ASSUME_NONNULL_END
```

```objective-cpp title="ios/NativeFuzzyProvider.mm"
#import "NativeFuzzyProvider.h"
#import <ReactCommon/CallInvoker.h>
#import <ReactCommon/TurboModule.h>
#import "NativeFuzzy.h"

@implementation NativeFuzzyProvider

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params {
  return std::make_shared<facebook::react::NativeFuzzy>(params.jsInvoker);
}

@end
```

Nem de Swift: os dois arquivos são cola, e a busca é o mesmo C++ nas duas plataformas.

## TypeScript

O hook entrega os títulos quando a lista muda e busca durante o render. `search` é síncrono, então o debounce da lista pesquisável pode sair:

```ts title="useFuzzy.ts"
import { useMemo } from 'react';
import NativeFuzzy from './specs/NativeFuzzy';

type Item = { id: string; title: string };

export function useFuzzy(items: Item[], query: string, limit = 50): Item[] {
  // setItems só substitui a lista, então rodá-lo de novo com os mesmos itens não faz mal.
  const loaded = useMemo(() => {
    NativeFuzzy.setItems(items.map((item) => item.title));
    return items;
  }, [items]);

  return useMemo(
    () => (query.trim() ? NativeFuzzy.search(query, limit).map((i) => loaded[i]) : loaded),
    [loaded, query, limit],
  );
}
```

O módulo guarda uma lista. É tudo de que esta tela precisa; duas telas buscando ao mesmo tempo pediriam uma lista por id.

No `SearchScreen`, as linhas do `useDebounced` e do `useMemo` viram uma só:

```tsx title="SearchScreen.tsx"
const visible = useFuzzy(items, state.query);
```

## Rode

O Codegen roda no `pod install` e no Gradle, então o header da spec em C++ existe antes de o C++ ser compilado:

```sh
cd ios && bundle exec pod install && cd ..
npx react-native run-ios
npx react-native run-android
```

O C++ merece o lugar quando o trabalho é computação, igual em toda plataforma, e chamado com frequência suficiente para que um quadro importe. Para o que conversa com a plataforma, Swift e Kotlin continuam as melhores ferramentas. A [documentação do React Native sobre C++ Turbo Native Modules](https://reactnative.dev/docs/the-new-architecture/pure-cxx-modules) cobre mais tipos, incluindo os seus próprios.]]></content:encoded>
            <author>rick@reactnative.land (React Native Land)</author>
        </item>
        <item>
            <title><![CDATA[Native code with Nitro Modules]]></title>
            <link>https://reactnative.land/posts/native-code-with-nitro-modules</link>
            <guid isPermaLink="false">https://reactnative.land/posts/native-code-with-nitro-modules.en</guid>
            <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[We built a battery module with [Expo Modules](/posts/native-code-with-expo-modules) and with [Turbo Modules](/posts/native-code-with-turbo-modules). Let's build it once more with Nitro Modules, from M]]></description>
            <content:encoded><![CDATA[We built a battery module with [Expo Modules](/posts/native-code-with-expo-modules) and with [Turbo Modules](/posts/native-code-with-turbo-modules). Let's build it once more with Nitro Modules, from Margelo. It still reads the battery level and tells the app when it changes.

Nitro keeps the Turbo Modules idea, a TypeScript spec that generates native interfaces, but its generator, Nitrogen, writes them for Swift and Kotlin directly. No Objective-C++ in between, no package to register by hand.

## Create the module

Nitrogen writes a library template with the build files already wired:

```sh
npx nitrogen@latest init react-native-battery
```

Trimmed to what matters here, it has this shape:

```
react-native-battery/
  nitro.json
  src/specs/Battery.nitro.ts
  src/index.ts
  ios/HybridBattery.swift
  android/src/main/java/com/margelo/nitro/battery/HybridBattery.kt
  nitrogen/generated/
```

`nitro.json` names the module's namespaces and which class implements each spec on each platform:

```json title="nitro.json"
{
  "cxxNamespace": ["battery"],
  "ios": { "iosModuleName": "NitroBattery" },
  "android": {
    "androidNamespace": ["battery"],
    "androidCxxLibName": "NitroBattery"
  },
  "autolinking": {
    "Battery": { "swift": "HybridBattery", "kotlin": "HybridBattery" }
  }
}
```

## The spec

A Nitro module is a hybrid object: a TypeScript interface, implemented natively, that JavaScript holds like any other object. The spec says which language implements it on each platform:

```ts title="src/specs/Battery.nitro.ts"
import type { HybridObject } from 'react-native-nitro-modules';

export interface Battery extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> {
  getLevel(): Promise<number>;
  addListener(onChange: (level: number) => void): () => void;
}
```

There are no events. A callback is a function like any other: JavaScript passes one to `addListener`, and native code hands back another that removes it. A method can also return a value at once, with no promise, when the native side has it at hand.

Nitrogen turns the spec into a Swift protocol and a Kotlin abstract class, both named `HybridBatterySpec`:

```sh
npx nitrogen
```

Its output goes in `nitrogen/generated/`. Run it again whenever the spec changes.

## Swift

The class implements the generated protocol, nothing more. `number` is `Double` on both platforms, and a spec method may throw:

```swift title="ios/HybridBattery.swift"
import NitroModules
import UIKit

class HybridBattery: HybridBatterySpec {
  func getLevel() throws -> Promise<Double> {
    return Promise.async {
      await MainActor.run {
        UIDevice.current.isBatteryMonitoringEnabled = true
        return Double(UIDevice.current.batteryLevel)
      }
    }
  }

  func addListener(onChange: @escaping (_ level: Double) -> Void) throws -> () -> Void {
    DispatchQueue.main.async {
      UIDevice.current.isBatteryMonitoringEnabled = true
    }
    let observer = NotificationCenter.default.addObserver(
      forName: UIDevice.batteryLevelDidChangeNotification,
      object: nil,
      queue: .main
    ) { _ in
      onChange(Double(UIDevice.current.batteryLevel))
    }
    return {
      NotificationCenter.default.removeObserver(observer)
    }
  }
}
```

`UIDevice` belongs to the main thread, so `MainActor.run` reads it there. A Nitro callback can be called from any thread; Nitro brings it back to JavaScript's. On the simulator the level is `-1`: it has no battery.

## Kotlin

The same two methods on Android, each listener with a broadcast receiver of its own:

```kotlin title="android/.../HybridBattery.kt"
package com.margelo.nitro.battery

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import androidx.annotation.Keep
import com.facebook.proguard.annotations.DoNotStrip
import com.margelo.nitro.NitroModules
import com.margelo.nitro.core.Promise

@Keep
@DoNotStrip
class HybridBattery : HybridBatterySpec() {
  private val context: Context
    get() = NitroModules.applicationContext ?: throw IllegalStateException("React Native is not ready")

  override fun getLevel(): Promise<Double> = Promise.async {
    val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
    intent?.let { levelOf(it) } ?: -1.0
  }

  override fun addListener(onChange: (level: Double) -> Unit): () -> Unit {
    val receiver = object : BroadcastReceiver() {
      override fun onReceive(context: Context, intent: Intent) {
        onChange(levelOf(intent))
      }
    }
    context.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
    return { context.unregisterReceiver(receiver) }
  }

  private fun levelOf(intent: Intent): Double {
    val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
    val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
    return if (level < 0 || scale <= 0) -1.0 else level / scale.toDouble()
  }
}
```

`@Keep` and `@DoNotStrip` stop R8 from removing a class that only native code refers to. Nothing listens unless JavaScript asked: the receiver lives from `addListener` until the function it returned is called.

## TypeScript

The module's entry creates the hybrid object by the name in `nitro.json`:

```ts title="src/index.ts"
import { NitroModules } from 'react-native-nitro-modules';
import type { Battery } from './specs/Battery.nitro';

export const battery = NitroModules.createHybridObject<Battery>('Battery');
```

The hook is the one from before, with the listener's own function to unsubscribe:

```ts title="useBatteryLevel.ts"
import { useEffect, useState } from 'react';
import { battery } from 'react-native-battery';

export function useBatteryLevel(): number | null {
  const [level, setLevel] = useState<number | null>(null);

  useEffect(() => {
    let active = true;
    battery.getLevel().then((value) => {
      if (active) setLevel(value);
    });
    const remove = battery.addListener((value) => setLevel(value));
    return () => {
      active = false;
      remove();
    };
  }, []);

  return level;
}
```

```tsx title="BatteryBadge.tsx"
import { Text } from 'react-native';
import { useBatteryLevel } from './useBatteryLevel';

export function BatteryBadge() {
  const level = useBatteryLevel();
  if (level === null || level < 0) return <Text>Battery: unknown</Text>;
  return <Text>Battery: {Math.round(level * 100)}%</Text>;
}
```

## Run it

The app needs Nitro's runtime beside the module, then a native build:

```sh
npm install react-native-nitro-modules ./react-native-battery
cd ios && bundle exec pod install && cd ..
npx react-native run-ios
npx react-native run-android
```

## Three ways, one battery

- **Expo Modules**: the least code. One definition per platform, Swift and Kotlin, hooks for the first and last listener, and nothing to generate.
- **Turbo Modules**: nothing beyond React Native. The spec is checked by Codegen at build time, but iOS goes through Objective-C++, and Android needs a package registered by hand.
- **Nitro Modules**: a spec like Turbo Modules, implemented straight in Swift and Kotlin. Callbacks are plain functions, and a hybrid object is an object, so there can be many. The cost is a dependency and a generator to run yourself.

The [Nitro docs](https://nitro.margelo.com/) go further: hybrid views, objects passed between modules, and modules in C++.]]></content:encoded>
            <author>rick@reactnative.land (React Native Land)</author>
        </item>
        <item>
            <title><![CDATA[Código nativo com Nitro Modules]]></title>
            <link>https://reactnative.land/pt/posts/native-code-with-nitro-modules</link>
            <guid isPermaLink="false">https://reactnative.land/posts/native-code-with-nitro-modules.pt-br</guid>
            <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Construímos um módulo de bateria com [Expo Modules](/pt/posts/native-code-with-expo-modules) e com [Turbo Modules](/pt/posts/native-code-with-turbo-modules). Vamos construí-lo mais uma vez com Nitro M]]></description>
            <content:encoded><![CDATA[Construímos um módulo de bateria com [Expo Modules](/pt/posts/native-code-with-expo-modules) e com [Turbo Modules](/pt/posts/native-code-with-turbo-modules). Vamos construí-lo mais uma vez com Nitro Modules, da Margelo. Ele continua lendo o nível da bateria e avisando o app quando ele muda.

O Nitro mantém a ideia dos Turbo Modules, uma spec em TypeScript que gera interfaces nativas, mas o gerador dele, o Nitrogen, as escreve direto para Swift e Kotlin. Sem Objective-C++ no meio, sem package para registrar à mão.

## Crie o módulo

O Nitrogen escreve um template de biblioteca com os arquivos de build já ligados:

```sh
npx nitrogen@latest init react-native-battery
```

Reduzido ao que importa aqui, ele tem esta forma:

```
react-native-battery/
  nitro.json
  src/specs/Battery.nitro.ts
  src/index.ts
  ios/HybridBattery.swift
  android/src/main/java/com/margelo/nitro/battery/HybridBattery.kt
  nitrogen/generated/
```

O `nitro.json` dá nome aos namespaces do módulo e diz qual classe implementa cada spec em cada plataforma:

```json title="nitro.json"
{
  "cxxNamespace": ["battery"],
  "ios": { "iosModuleName": "NitroBattery" },
  "android": {
    "androidNamespace": ["battery"],
    "androidCxxLibName": "NitroBattery"
  },
  "autolinking": {
    "Battery": { "swift": "HybridBattery", "kotlin": "HybridBattery" }
  }
}
```

## A spec

Um módulo Nitro é um hybrid object: uma interface TypeScript, implementada no nativo, que o JavaScript segura como qualquer outro objeto. A spec diz qual linguagem a implementa em cada plataforma:

```ts title="src/specs/Battery.nitro.ts"
import type { HybridObject } from 'react-native-nitro-modules';

export interface Battery extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> {
  getLevel(): Promise<number>;
  addListener(onChange: (level: number) => void): () => void;
}
```

Não há eventos. Um callback é uma função como outra qualquer: o JavaScript passa uma para o `addListener`, e o código nativo devolve outra que a remove. Um método também pode devolver um valor na hora, sem promise, quando o lado nativo o tem à mão.

O Nitrogen transforma a spec num protocolo Swift e numa classe abstrata Kotlin, ambos chamados `HybridBatterySpec`:

```sh
npx nitrogen
```

A saída vai para `nitrogen/generated/`. Rode de novo sempre que a spec mudar.

## Swift

A classe implementa o protocolo gerado, nada mais. `number` é `Double` nas duas plataformas, e um método da spec pode lançar erro:

```swift title="ios/HybridBattery.swift"
import NitroModules
import UIKit

class HybridBattery: HybridBatterySpec {
  func getLevel() throws -> Promise<Double> {
    return Promise.async {
      await MainActor.run {
        UIDevice.current.isBatteryMonitoringEnabled = true
        return Double(UIDevice.current.batteryLevel)
      }
    }
  }

  func addListener(onChange: @escaping (_ level: Double) -> Void) throws -> () -> Void {
    DispatchQueue.main.async {
      UIDevice.current.isBatteryMonitoringEnabled = true
    }
    let observer = NotificationCenter.default.addObserver(
      forName: UIDevice.batteryLevelDidChangeNotification,
      object: nil,
      queue: .main
    ) { _ in
      onChange(Double(UIDevice.current.batteryLevel))
    }
    return {
      NotificationCenter.default.removeObserver(observer)
    }
  }
}
```

`UIDevice` pertence à thread principal, então `MainActor.run` o lê nela. Um callback do Nitro pode ser chamado de qualquer thread; o Nitro o leva de volta para a do JavaScript. No simulador o nível é `-1`: ele não tem bateria.

## Kotlin

Os mesmos dois métodos no Android, cada listener com um broadcast receiver próprio:

```kotlin title="android/.../HybridBattery.kt"
package com.margelo.nitro.battery

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import androidx.annotation.Keep
import com.facebook.proguard.annotations.DoNotStrip
import com.margelo.nitro.NitroModules
import com.margelo.nitro.core.Promise

@Keep
@DoNotStrip
class HybridBattery : HybridBatterySpec() {
  private val context: Context
    get() = NitroModules.applicationContext ?: throw IllegalStateException("React Native is not ready")

  override fun getLevel(): Promise<Double> = Promise.async {
    val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
    intent?.let { levelOf(it) } ?: -1.0
  }

  override fun addListener(onChange: (level: Double) -> Unit): () -> Unit {
    val receiver = object : BroadcastReceiver() {
      override fun onReceive(context: Context, intent: Intent) {
        onChange(levelOf(intent))
      }
    }
    context.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
    return { context.unregisterReceiver(receiver) }
  }

  private fun levelOf(intent: Intent): Double {
    val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
    val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
    return if (level < 0 || scale <= 0) -1.0 else level / scale.toDouble()
  }
}
```

`@Keep` e `@DoNotStrip` impedem o R8 de remover uma classe que só o código nativo referencia. Nada fica escutando sem o JavaScript pedir: o receiver vive do `addListener` até a função que ele devolveu ser chamada.

## TypeScript

A entrada do módulo cria o hybrid object pelo nome dado no `nitro.json`:

```ts title="src/index.ts"
import { NitroModules } from 'react-native-nitro-modules';
import type { Battery } from './specs/Battery.nitro';

export const battery = NitroModules.createHybridObject<Battery>('Battery');
```

O hook é o de antes, com a função do próprio listener para cancelar a inscrição:

```ts title="useBatteryLevel.ts"
import { useEffect, useState } from 'react';
import { battery } from 'react-native-battery';

export function useBatteryLevel(): number | null {
  const [level, setLevel] = useState<number | null>(null);

  useEffect(() => {
    let active = true;
    battery.getLevel().then((value) => {
      if (active) setLevel(value);
    });
    const remove = battery.addListener((value) => setLevel(value));
    return () => {
      active = false;
      remove();
    };
  }, []);

  return level;
}
```

```tsx title="BatteryBadge.tsx"
import { Text } from 'react-native';
import { useBatteryLevel } from './useBatteryLevel';

export function BatteryBadge() {
  const level = useBatteryLevel();
  if (level === null || level < 0) return <Text>Bateria: desconhecida</Text>;
  return <Text>Bateria: {Math.round(level * 100)}%</Text>;
}
```

## Rode

O app precisa do runtime do Nitro ao lado do módulo, e depois de um build nativo:

```sh
npm install react-native-nitro-modules ./react-native-battery
cd ios && bundle exec pod install && cd ..
npx react-native run-ios
npx react-native run-android
```

## Três caminhos, uma bateria

- **Expo Modules**: o menor código. Uma definição por plataforma, Swift e Kotlin, ganchos para o primeiro e o último listener, e nada para gerar.
- **Turbo Modules**: nada além do React Native. A spec é conferida pelo Codegen no build, mas o iOS passa por Objective-C++, e o Android pede um package registrado à mão.
- **Nitro Modules**: uma spec como a dos Turbo Modules, implementada direto em Swift e Kotlin. Callbacks são funções comuns, e um hybrid object é um objeto, então pode haver vários. O custo é uma dependência e um gerador que você mesmo roda.

A [documentação do Nitro](https://nitro.margelo.com/) vai além: hybrid views, objetos passados entre módulos e módulos em C++.]]></content:encoded>
            <author>rick@reactnative.land (React Native Land)</author>
        </item>
        <item>
            <title><![CDATA[Native code with Turbo Modules]]></title>
            <link>https://reactnative.land/posts/native-code-with-turbo-modules</link>
            <guid isPermaLink="false">https://reactnative.land/posts/native-code-with-turbo-modules.en</guid>
            <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In [the last post](/posts/native-code-with-expo-modules) we built a battery module with the Expo Modules API. Let's build the same one with what React Native itself ships: a Turbo Native Module. It re]]></description>
            <content:encoded><![CDATA[In [the last post](/posts/native-code-with-expo-modules) we built a battery module with the Expo Modules API. Let's build the same one with what React Native itself ships: a Turbo Native Module. It reads the battery level and tells the app when it changes.

The difference starts with where the truth lives. Here, a TypeScript spec describes the module, and Codegen turns it into native interfaces that Kotlin and Objective-C++ have to implement. If they don't match, the build fails.

## The shape

In a React Native app, trimmed to what matters here:

```
specs/NativeBattery.ts
package.json
ios/RCTNativeBattery.h
ios/RCTNativeBattery.mm
ios/BatteryLevel.swift
android/app/src/main/java/com/nativebattery/NativeBatteryModule.kt
android/app/src/main/java/com/nativebattery/NativeBatteryPackage.kt
```

## The spec

The spec's file name must start with `Native`. It declares a promise and a typed event:

```ts title="specs/NativeBattery.ts"
import type { CodegenTypes, TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export type BatteryEvent = {
  level: number;
};

export interface Spec extends TurboModule {
  getLevel(): Promise<number>;
  readonly onLevelChange: CodegenTypes.EventEmitter<BatteryEvent>;
}

export default TurboModuleRegistry.getEnforcing<Spec>('NativeBattery');
```

`package.json` tells Codegen where the specs are, the Java package to generate into, and which Objective-C class provides the module:

```json title="package.json"
{
  "codegenConfig": {
    "name": "NativeBatterySpec",
    "type": "modules",
    "jsSrcsDir": "specs",
    "android": { "javaPackageName": "com.nativebattery" },
    "ios": { "modulesProvider": { "NativeBattery": "RCTNativeBattery" } }
  }
}
```

## Kotlin

Codegen writes an abstract class, `NativeBatterySpec`, with one method per function in the spec and an `emitOnLevelChange` for the event. The module extends it:

```kotlin title="android/.../NativeBatteryModule.kt"
package com.nativebattery

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext

class NativeBatteryModule(reactContext: ReactApplicationContext) : NativeBatterySpec(reactContext) {
  private val receiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
      emitOnLevelChange(Arguments.createMap().apply { putDouble("level", levelOf(intent)) })
    }
  }

  override fun getName() = NAME

  override fun initialize() {
    super.initialize()
    reactApplicationContext.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
  }

  override fun invalidate() {
    reactApplicationContext.unregisterReceiver(receiver)
    super.invalidate()
  }

  override fun getLevel(promise: Promise) {
    val intent = reactApplicationContext.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
    promise.resolve(intent?.let { levelOf(it) } ?: -1.0)
  }

  private fun levelOf(intent: Intent): Double {
    val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
    val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
    return if (level < 0 || scale <= 0) -1.0 else level / scale.toDouble()
  }

  companion object {
    const val NAME = "NativeBattery"
  }
}
```

There is no first-listener hook as in Expo Modules, so the receiver lives as long as the module: from the first time JavaScript asks for it until the app reloads.

A package hands the module to React Native when JavaScript asks for its name:

```kotlin title="android/.../NativeBatteryPackage.kt"
package com.nativebattery

import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider

class NativeBatteryPackage : BaseReactPackage() {
  override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
    if (name == NativeBatteryModule.NAME) NativeBatteryModule(reactContext) else null

  override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
    mapOf(
      NativeBatteryModule.NAME to ReactModuleInfo(
        name = NativeBatteryModule.NAME,
        className = NativeBatteryModule.NAME,
        canOverrideExistingModule = false,
        needsEagerInit = false,
        isCxxModule = false,
        isTurboModule = true,
      )
    )
  }
}
```

And `MainApplication.kt` adds it to the list autolinking builds:

```kotlin title="MainApplication.kt"
override fun getPackages(): List<ReactPackage> =
  PackageList(this).packages.apply {
    add(NativeBatteryPackage())
  }
```

## Swift

Codegen writes Objective-C++ protocols, which Swift cannot adopt. The battery code can still be Swift: a small class, marked `@objc` so Objective-C sees it:

```swift title="ios/BatteryLevel.swift"
import UIKit

@objc public class BatteryLevel: NSObject {
  private var observer: NSObjectProtocol?

  @objc public static var current: Float {
    UIDevice.current.isBatteryMonitoringEnabled = true
    return UIDevice.current.batteryLevel
  }

  @objc public func start(_ onChange: @escaping (Float) -> Void) {
    UIDevice.current.isBatteryMonitoringEnabled = true
    observer = NotificationCenter.default.addObserver(
      forName: UIDevice.batteryLevelDidChangeNotification,
      object: nil,
      queue: .main
    ) { _ in
      onChange(UIDevice.current.batteryLevel)
    }
  }

  @objc public func stop() {
    if let observer {
      NotificationCenter.default.removeObserver(observer)
    }
    observer = nil
  }
}
```

## Objective-C++

The module itself is a thin Objective-C++ class. Its header adopts the generated protocol, and extends the generated base class that brings `emitOnLevelChange`:

```objective-c title="ios/RCTNativeBattery.h"
#import <NativeBatterySpec/NativeBatterySpec.h>

NS_ASSUME_NONNULL_BEGIN

@interface RCTNativeBattery : NativeBatterySpecBase <NativeBatterySpec>
@end

NS_ASSUME_NONNULL_END
```

The implementation forwards to Swift. `UIDevice` belongs to the main thread, so both calls go there; on the simulator the level is `-1`, since it has no battery:

```objective-cpp title="ios/RCTNativeBattery.mm"
#import "RCTNativeBattery.h"
#import "BatteryApp-Swift.h"

@implementation RCTNativeBattery {
  BatteryLevel *_battery;
}

+ (NSString *)moduleName {
  return @"NativeBattery";
}

- (instancetype)init {
  if (self = [super init]) {
    _battery = [BatteryLevel new];
    __weak RCTNativeBattery *weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
      [weakSelf->_battery start:^(float level) {
        [weakSelf emitOnLevelChange:@{@"level": @(level)}];
      }];
    });
  }
  return self;
}

- (void)dealloc {
  [_battery stop];
}

- (void)getLevel:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  dispatch_async(dispatch_get_main_queue(), ^{
    resolve(@(BatteryLevel.current));
  });
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params {
  return std::make_shared<facebook::react::NativeBatterySpecJSI>(params);
}

@end
```

`BatteryApp-Swift.h` is the header Xcode writes for the app's Swift code; its name is your target's.

## TypeScript

The spec is already the typed module, so the hook imports it directly. An event is a function that takes a listener and returns a subscription:

```ts title="useBatteryLevel.ts"
import { useEffect, useState } from 'react';
import NativeBattery from './specs/NativeBattery';

export function useBatteryLevel(): number | null {
  const [level, setLevel] = useState<number | null>(null);

  useEffect(() => {
    let active = true;
    NativeBattery.getLevel().then((value) => {
      if (active) setLevel(value);
    });
    const subscription = NativeBattery.onLevelChange((event) => setLevel(event.level));
    return () => {
      active = false;
      subscription.remove();
    };
  }, []);

  return level;
}
```

The badge is the same as before:

```tsx title="BatteryBadge.tsx"
import { Text } from 'react-native';
import { useBatteryLevel } from './useBatteryLevel';

export function BatteryBadge() {
  const level = useBatteryLevel();
  if (level === null || level < 0) return <Text>Battery: unknown</Text>;
  return <Text>Battery: {Math.round(level * 100)}%</Text>;
}
```

## Run it

Codegen runs when the pods are installed and when Gradle builds, so a change to the spec needs both again:

```sh
cd ios && bundle exec pod install && cd ..
npx react-native run-ios
npx react-native run-android
```

## Which one?

- The spec is the contract: Codegen checks the native side against it at build time, and the module is typed in JavaScript with no declaration of your own.
- Turbo Modules need no dependency beyond React Native, and can be written in C++ once for both platforms.
- Expo Modules ask for less code: no package, no registration, no Objective-C++, Swift all the way, and hooks for the first and last listener.

The [React Native docs on Turbo Native Modules](https://reactnative.dev/docs/turbo-native-modules-introduction) go further, including modules written in C++.]]></content:encoded>
            <author>rick@reactnative.land (React Native Land)</author>
        </item>
        <item>
            <title><![CDATA[Código nativo com Turbo Modules]]></title>
            <link>https://reactnative.land/pt/posts/native-code-with-turbo-modules</link>
            <guid isPermaLink="false">https://reactnative.land/posts/native-code-with-turbo-modules.pt-br</guid>
            <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[No [post anterior](/pt/posts/native-code-with-expo-modules) construímos um módulo de bateria com a Expo Modules API. Vamos construir o mesmo com o que o próprio React Native oferece: um Turbo Native M]]></description>
            <content:encoded><![CDATA[No [post anterior](/pt/posts/native-code-with-expo-modules) construímos um módulo de bateria com a Expo Modules API. Vamos construir o mesmo com o que o próprio React Native oferece: um Turbo Native Module. Ele lê o nível da bateria e avisa o app quando ele muda.

A diferença começa em onde fica a verdade. Aqui, uma spec em TypeScript descreve o módulo, e o Codegen a transforma em interfaces nativas que o Kotlin e o Objective-C++ precisam implementar. Se não baterem, o build falha.

## A forma

Num app React Native, reduzido ao que importa aqui:

```
specs/NativeBattery.ts
package.json
ios/RCTNativeBattery.h
ios/RCTNativeBattery.mm
ios/BatteryLevel.swift
android/app/src/main/java/com/nativebattery/NativeBatteryModule.kt
android/app/src/main/java/com/nativebattery/NativeBatteryPackage.kt
```

## A spec

O nome do arquivo da spec precisa começar com `Native`. Ela declara uma promise e um evento tipado:

```ts title="specs/NativeBattery.ts"
import type { CodegenTypes, TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export type BatteryEvent = {
  level: number;
};

export interface Spec extends TurboModule {
  getLevel(): Promise<number>;
  readonly onLevelChange: CodegenTypes.EventEmitter<BatteryEvent>;
}

export default TurboModuleRegistry.getEnforcing<Spec>('NativeBattery');
```

O `package.json` diz ao Codegen onde estão as specs, em que pacote Java gerar o código e qual classe Objective-C fornece o módulo:

```json title="package.json"
{
  "codegenConfig": {
    "name": "NativeBatterySpec",
    "type": "modules",
    "jsSrcsDir": "specs",
    "android": { "javaPackageName": "com.nativebattery" },
    "ios": { "modulesProvider": { "NativeBattery": "RCTNativeBattery" } }
  }
}
```

## Kotlin

O Codegen escreve uma classe abstrata, `NativeBatterySpec`, com um método para cada função da spec e um `emitOnLevelChange` para o evento. O módulo a estende:

```kotlin title="android/.../NativeBatteryModule.kt"
package com.nativebattery

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext

class NativeBatteryModule(reactContext: ReactApplicationContext) : NativeBatterySpec(reactContext) {
  private val receiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
      emitOnLevelChange(Arguments.createMap().apply { putDouble("level", levelOf(intent)) })
    }
  }

  override fun getName() = NAME

  override fun initialize() {
    super.initialize()
    reactApplicationContext.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
  }

  override fun invalidate() {
    reactApplicationContext.unregisterReceiver(receiver)
    super.invalidate()
  }

  override fun getLevel(promise: Promise) {
    val intent = reactApplicationContext.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
    promise.resolve(intent?.let { levelOf(it) } ?: -1.0)
  }

  private fun levelOf(intent: Intent): Double {
    val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
    val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
    return if (level < 0 || scale <= 0) -1.0 else level / scale.toDouble()
  }

  companion object {
    const val NAME = "NativeBattery"
  }
}
```

Não há um gancho de primeiro listener como no Expo Modules, então o receiver vive tanto quanto o módulo: da primeira vez que o JavaScript o pede até o app recarregar.

Um package entrega o módulo ao React Native quando o JavaScript pede pelo nome dele:

```kotlin title="android/.../NativeBatteryPackage.kt"
package com.nativebattery

import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider

class NativeBatteryPackage : BaseReactPackage() {
  override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
    if (name == NativeBatteryModule.NAME) NativeBatteryModule(reactContext) else null

  override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
    mapOf(
      NativeBatteryModule.NAME to ReactModuleInfo(
        name = NativeBatteryModule.NAME,
        className = NativeBatteryModule.NAME,
        canOverrideExistingModule = false,
        needsEagerInit = false,
        isCxxModule = false,
        isTurboModule = true,
      )
    )
  }
}
```

E o `MainApplication.kt` o adiciona à lista que o autolinking monta:

```kotlin title="MainApplication.kt"
override fun getPackages(): List<ReactPackage> =
  PackageList(this).packages.apply {
    add(NativeBatteryPackage())
  }
```

## Swift

O Codegen escreve protocolos Objective-C++, que o Swift não consegue adotar. O código da bateria ainda pode ser Swift: uma classe pequena, marcada com `@objc` para que o Objective-C a enxergue:

```swift title="ios/BatteryLevel.swift"
import UIKit

@objc public class BatteryLevel: NSObject {
  private var observer: NSObjectProtocol?

  @objc public static var current: Float {
    UIDevice.current.isBatteryMonitoringEnabled = true
    return UIDevice.current.batteryLevel
  }

  @objc public func start(_ onChange: @escaping (Float) -> Void) {
    UIDevice.current.isBatteryMonitoringEnabled = true
    observer = NotificationCenter.default.addObserver(
      forName: UIDevice.batteryLevelDidChangeNotification,
      object: nil,
      queue: .main
    ) { _ in
      onChange(UIDevice.current.batteryLevel)
    }
  }

  @objc public func stop() {
    if let observer {
      NotificationCenter.default.removeObserver(observer)
    }
    observer = nil
  }
}
```

## Objective-C++

O módulo em si é uma classe Objective-C++ fina. O header adota o protocolo gerado e estende a classe base gerada, que traz o `emitOnLevelChange`:

```objective-c title="ios/RCTNativeBattery.h"
#import <NativeBatterySpec/NativeBatterySpec.h>

NS_ASSUME_NONNULL_BEGIN

@interface RCTNativeBattery : NativeBatterySpecBase <NativeBatterySpec>
@end

NS_ASSUME_NONNULL_END
```

A implementação repassa para o Swift. `UIDevice` pertence à thread principal, então as duas chamadas vão para ela; no simulador o nível é `-1`, já que ele não tem bateria:

```objective-cpp title="ios/RCTNativeBattery.mm"
#import "RCTNativeBattery.h"
#import "BatteryApp-Swift.h"

@implementation RCTNativeBattery {
  BatteryLevel *_battery;
}

+ (NSString *)moduleName {
  return @"NativeBattery";
}

- (instancetype)init {
  if (self = [super init]) {
    _battery = [BatteryLevel new];
    __weak RCTNativeBattery *weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
      [weakSelf->_battery start:^(float level) {
        [weakSelf emitOnLevelChange:@{@"level": @(level)}];
      }];
    });
  }
  return self;
}

- (void)dealloc {
  [_battery stop];
}

- (void)getLevel:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  dispatch_async(dispatch_get_main_queue(), ^{
    resolve(@(BatteryLevel.current));
  });
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params {
  return std::make_shared<facebook::react::NativeBatterySpecJSI>(params);
}

@end
```

`BatteryApp-Swift.h` é o header que o Xcode escreve para o código Swift do app; o nome dele é o do seu target.

## TypeScript

A spec já é o módulo tipado, então o hook a importa diretamente. Um evento é uma função que recebe um listener e devolve uma inscrição:

```ts title="useBatteryLevel.ts"
import { useEffect, useState } from 'react';
import NativeBattery from './specs/NativeBattery';

export function useBatteryLevel(): number | null {
  const [level, setLevel] = useState<number | null>(null);

  useEffect(() => {
    let active = true;
    NativeBattery.getLevel().then((value) => {
      if (active) setLevel(value);
    });
    const subscription = NativeBattery.onLevelChange((event) => setLevel(event.level));
    return () => {
      active = false;
      subscription.remove();
    };
  }, []);

  return level;
}
```

O badge é o mesmo de antes:

```tsx title="BatteryBadge.tsx"
import { Text } from 'react-native';
import { useBatteryLevel } from './useBatteryLevel';

export function BatteryBadge() {
  const level = useBatteryLevel();
  if (level === null || level < 0) return <Text>Bateria: desconhecida</Text>;
  return <Text>Bateria: {Math.round(level * 100)}%</Text>;
}
```

## Rode

O Codegen roda quando os pods são instalados e quando o Gradle compila, então uma mudança na spec pede os dois de novo:

```sh
cd ios && bundle exec pod install && cd ..
npx react-native run-ios
npx react-native run-android
```

## Qual escolher?

- A spec é o contrato: o Codegen confere o lado nativo contra ela no build, e o módulo já vem tipado no JavaScript, sem declaração sua.
- Turbo Modules não pedem dependência além do React Native, e podem ser escritos uma vez em C++ para as duas plataformas.
- Expo Modules pedem menos código: sem package, sem registro, sem Objective-C++, Swift do começo ao fim, e ganchos para o primeiro e o último listener.

A [documentação do React Native sobre Turbo Native Modules](https://reactnative.dev/docs/turbo-native-modules-introduction) vai além, incluindo módulos escritos em C++.]]></content:encoded>
            <author>rick@reactnative.land (React Native Land)</author>
        </item>
        <item>
            <title><![CDATA[Native code with Expo Modules]]></title>
            <link>https://reactnative.land/posts/native-code-with-expo-modules</link>
            <guid isPermaLink="false">https://reactnative.land/posts/native-code-with-expo-modules.en</guid>
            <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Sometimes JavaScript is not enough: you need an API only the platform has. The Expo Modules API lets you write that code in Swift and Kotlin with the same shape on both sides, and call it from TypeScr]]></description>
            <content:encoded><![CDATA[Sometimes JavaScript is not enough: you need an API only the platform has. The Expo Modules API lets you write that code in Swift and Kotlin with the same shape on both sides, and call it from TypeScript like any other module.

Let's build a small one. It reads the battery level and tells the app when it changes.

## Create the module

In an Expo app, a local module lives in `modules/` and is linked on the next native build:

```sh
npx create-expo-module@latest --local
```

Name it `battery`. Trimmed to what matters here, it has this shape:

```
modules/battery/
  expo-module.config.json
  index.ts
  src/BatteryModule.ts
  ios/BatteryModule.swift
  android/src/main/java/expo/modules/battery/BatteryModule.kt
```

The config file tells autolinking which class to load on each platform:

```json title="expo-module.config.json"
{
  "platforms": ["apple", "android"],
  "apple": { "modules": ["BatteryModule"] },
  "android": { "modules": ["expo.modules.battery.BatteryModule"] }
}
```

## Swift

A module is a class with a `definition()`. `Name` is what JavaScript asks for, `AsyncFunction` returns a promise, and `Events` declares what the module can send:

```swift title="ios/BatteryModule.swift"
import ExpoModulesCore
import UIKit

public class BatteryModule: Module {
  private var observer: NSObjectProtocol?

  public func definition() -> ModuleDefinition {
    Name("Battery")

    Events("onLevelChange")

    AsyncFunction("getLevelAsync") { () -> Float in
      UIDevice.current.isBatteryMonitoringEnabled = true
      return UIDevice.current.batteryLevel
    }.runOnQueue(.main)

    OnStartObserving {
      UIDevice.current.isBatteryMonitoringEnabled = true
      self.observer = NotificationCenter.default.addObserver(
        forName: UIDevice.batteryLevelDidChangeNotification,
        object: nil,
        queue: .main
      ) { [weak self] _ in
        self?.sendEvent("onLevelChange", ["level": UIDevice.current.batteryLevel])
      }
    }

    OnStopObserving {
      if let observer = self.observer {
        NotificationCenter.default.removeObserver(observer)
      }
      self.observer = nil
    }
  }
}
```

`UIDevice` belongs to the main thread, so `runOnQueue(.main)` runs the function there. On the simulator the level is `-1`: it has no battery.

## Kotlin

The same definition, word for word, on Android. There is no battery-level callback to observe, so the module listens to the system's `ACTION_BATTERY_CHANGED` broadcast:

```kotlin title="android/.../BatteryModule.kt"
package expo.modules.battery

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition

class BatteryModule : Module() {
  private val context: Context
    get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()

  private val receiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
      sendEvent("onLevelChange", mapOf("level" to levelOf(intent)))
    }
  }

  override fun definition() = ModuleDefinition {
    Name("Battery")

    Events("onLevelChange")

    AsyncFunction("getLevelAsync") {
      val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
      intent?.let { levelOf(it) } ?: -1f
    }

    OnStartObserving {
      context.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
    }

    OnStopObserving {
      context.unregisterReceiver(receiver)
    }
  }

  private fun levelOf(intent: Intent): Float {
    val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
    val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
    return if (level < 0 || scale <= 0) -1f else level / scale.toFloat()
  }
}
```

`OnStartObserving` and `OnStopObserving` run when JavaScript adds its first listener and removes its last, so nothing listens while no one is interested.

## TypeScript

One declaration types both platforms:

```ts title="src/BatteryModule.ts"
import { NativeModule, requireNativeModule } from 'expo';

type BatteryEvents = {
  onLevelChange(event: { level: number }): void;
};

declare class BatteryModule extends NativeModule<BatteryEvents> {
  getLevelAsync(): Promise<number>;
}

export default requireNativeModule<BatteryModule>('Battery');
```

The module's `index.ts` re-exports it, and a hook hides the subscription:

```ts title="useBatteryLevel.ts"
import { useEffect, useState } from 'react';
import Battery from './modules/battery';

export function useBatteryLevel(): number | null {
  const [level, setLevel] = useState<number | null>(null);

  useEffect(() => {
    let active = true;
    Battery.getLevelAsync().then((value) => {
      if (active) setLevel(value);
    });
    const subscription = Battery.addListener('onLevelChange', (event) => setLevel(event.level));
    return () => {
      active = false;
      subscription.remove();
    };
  }, []);

  return level;
}
```

```tsx title="BatteryBadge.tsx"
import { Text } from 'react-native';
import { useBatteryLevel } from './useBatteryLevel';

export function BatteryBadge() {
  const level = useBatteryLevel();
  if (level === null || level < 0) return <Text>Battery: unknown</Text>;
  return <Text>Battery: {Math.round(level * 100)}%</Text>;
}
```

## Run it

Expo Go has only the native code it ships with, so a module of your own needs a development build:

```sh
npx expo run:ios
npx expo run:android
```

That's it: one definition in two languages, and one typed module in JavaScript. The [Expo Modules API docs](https://docs.expo.dev/modules/overview/) cover views, records and config plugins.]]></content:encoded>
            <author>rick@reactnative.land (React Native Land)</author>
        </item>
        <item>
            <title><![CDATA[Código nativo com Expo Modules]]></title>
            <link>https://reactnative.land/pt/posts/native-code-with-expo-modules</link>
            <guid isPermaLink="false">https://reactnative.land/posts/native-code-with-expo-modules.pt-br</guid>
            <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Às vezes o JavaScript não basta: você precisa de uma API que só a plataforma tem. A Expo Modules API permite escrever esse código em Swift e Kotlin com o mesmo formato dos dois lados, e chamá-lo do Ty]]></description>
            <content:encoded><![CDATA[Às vezes o JavaScript não basta: você precisa de uma API que só a plataforma tem. A Expo Modules API permite escrever esse código em Swift e Kotlin com o mesmo formato dos dois lados, e chamá-lo do TypeScript como qualquer outro módulo.

Vamos construir um pequeno. Ele lê o nível da bateria e avisa o app quando ele muda.

## Crie o módulo

Num app Expo, um módulo local fica em `modules/` e é ligado no próximo build nativo:

```sh
npx create-expo-module@latest --local
```

Dê a ele o nome `battery`. Reduzido ao que importa aqui, ele tem esta forma:

```
modules/battery/
  expo-module.config.json
  index.ts
  src/BatteryModule.ts
  ios/BatteryModule.swift
  android/src/main/java/expo/modules/battery/BatteryModule.kt
```

O arquivo de configuração diz ao autolinking qual classe carregar em cada plataforma:

```json title="expo-module.config.json"
{
  "platforms": ["apple", "android"],
  "apple": { "modules": ["BatteryModule"] },
  "android": { "modules": ["expo.modules.battery.BatteryModule"] }
}
```

## Swift

Um módulo é uma classe com uma `definition()`. `Name` é o nome que o JavaScript pede, `AsyncFunction` devolve uma promise e `Events` declara o que o módulo pode enviar:

```swift title="ios/BatteryModule.swift"
import ExpoModulesCore
import UIKit

public class BatteryModule: Module {
  private var observer: NSObjectProtocol?

  public func definition() -> ModuleDefinition {
    Name("Battery")

    Events("onLevelChange")

    AsyncFunction("getLevelAsync") { () -> Float in
      UIDevice.current.isBatteryMonitoringEnabled = true
      return UIDevice.current.batteryLevel
    }.runOnQueue(.main)

    OnStartObserving {
      UIDevice.current.isBatteryMonitoringEnabled = true
      self.observer = NotificationCenter.default.addObserver(
        forName: UIDevice.batteryLevelDidChangeNotification,
        object: nil,
        queue: .main
      ) { [weak self] _ in
        self?.sendEvent("onLevelChange", ["level": UIDevice.current.batteryLevel])
      }
    }

    OnStopObserving {
      if let observer = self.observer {
        NotificationCenter.default.removeObserver(observer)
      }
      self.observer = nil
    }
  }
}
```

`UIDevice` pertence à thread principal, então `runOnQueue(.main)` executa a função nela. No simulador o nível é `-1`: ele não tem bateria.

## Kotlin

A mesma definição, palavra por palavra, no Android. Não há um callback de nível de bateria para observar, então o módulo escuta o broadcast `ACTION_BATTERY_CHANGED` do sistema:

```kotlin title="android/.../BatteryModule.kt"
package expo.modules.battery

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition

class BatteryModule : Module() {
  private val context: Context
    get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()

  private val receiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
      sendEvent("onLevelChange", mapOf("level" to levelOf(intent)))
    }
  }

  override fun definition() = ModuleDefinition {
    Name("Battery")

    Events("onLevelChange")

    AsyncFunction("getLevelAsync") {
      val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
      intent?.let { levelOf(it) } ?: -1f
    }

    OnStartObserving {
      context.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
    }

    OnStopObserving {
      context.unregisterReceiver(receiver)
    }
  }

  private fun levelOf(intent: Intent): Float {
    val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
    val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
    return if (level < 0 || scale <= 0) -1f else level / scale.toFloat()
  }
}
```

`OnStartObserving` e `OnStopObserving` rodam quando o JavaScript adiciona o primeiro listener e remove o último, então nada fica escutando enquanto ninguém tem interesse.

## TypeScript

Uma só declaração tipa as duas plataformas:

```ts title="src/BatteryModule.ts"
import { NativeModule, requireNativeModule } from 'expo';

type BatteryEvents = {
  onLevelChange(event: { level: number }): void;
};

declare class BatteryModule extends NativeModule<BatteryEvents> {
  getLevelAsync(): Promise<number>;
}

export default requireNativeModule<BatteryModule>('Battery');
```

O `index.ts` do módulo a reexporta, e um hook esconde a inscrição:

```ts title="useBatteryLevel.ts"
import { useEffect, useState } from 'react';
import Battery from './modules/battery';

export function useBatteryLevel(): number | null {
  const [level, setLevel] = useState<number | null>(null);

  useEffect(() => {
    let active = true;
    Battery.getLevelAsync().then((value) => {
      if (active) setLevel(value);
    });
    const subscription = Battery.addListener('onLevelChange', (event) => setLevel(event.level));
    return () => {
      active = false;
      subscription.remove();
    };
  }, []);

  return level;
}
```

```tsx title="BatteryBadge.tsx"
import { Text } from 'react-native';
import { useBatteryLevel } from './useBatteryLevel';

export function BatteryBadge() {
  const level = useBatteryLevel();
  if (level === null || level < 0) return <Text>Bateria: desconhecida</Text>;
  return <Text>Bateria: {Math.round(level * 100)}%</Text>;
}
```

## Rode

O Expo Go só tem o código nativo com que é distribuído, então um módulo seu precisa de um development build:

```sh
npx expo run:ios
npx expo run:android
```

Pronto: uma definição em duas linguagens e um módulo tipado no JavaScript. A [documentação da Expo Modules API](https://docs.expo.dev/modules/overview/) cobre views, records e config plugins.]]></content:encoded>
            <author>rick@reactnative.land (React Native Land)</author>
        </item>
        <item>
            <title><![CDATA[A searchable list in React Native]]></title>
            <link>https://reactnative.land/posts/a-searchable-list</link>
            <guid isPermaLink="false">https://reactnative.land/posts/a-searchable-list.en</guid>
            <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Most apps have a list you can search. Let's build one in steps, from the smallest component to a list that stays fast with thousands of rows.  ## Start small  A row is a `Text` inside a `Pressable`:  ]]></description>
            <content:encoded><![CDATA[Most apps have a list you can search. Let's build one in steps, from the smallest component to a list that stays fast with thousands of rows.

## Start small

A row is a `Text` inside a `Pressable`:

```tsx title="Row.tsx"
import { Pressable, Text } from 'react-native';

export function Row({ title, onPress }: { title: string; onPress: () => void }) {
  return (
    <Pressable onPress={onPress}>
      <Text>{title}</Text>
    </Pressable>
  );
}
```

Create the app with Expo first:

```sh
npx create-expo-app@latest searchable-list
cd searchable-list && npx expo start
```

## Wait for the typing to stop

Filtering on every keystroke is wasteful. A generic hook hands back the value only after it has been still for a moment:

```ts title="useDebounced.ts"
import { useEffect, useState } from 'react';

export function useDebounced<T>(value: T, delay = 250): T {
  const [settled, setSettled] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setSettled(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return settled;
}
```

## The list

The screen keeps its state in a reducer, filters with `useMemo`, and renders with a `FlatList` whose rows are memoized, so typing re-renders only what changed:

```tsx title="SearchScreen.tsx"
import { memo, useCallback, useMemo, useReducer } from 'react';
import { FlatList, StyleSheet, TextInput, View } from 'react-native';
import { Row } from './Row';
import { useDebounced } from './useDebounced';

type Item = { id: string; title: string };

type State = { query: string; selected: Set<string> };
type Action =
  | { type: 'query'; query: string }
  | { type: 'toggle'; id: string };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'query':
      return { ...state, query: action.query };
    case 'toggle': {
      const selected = new Set(state.selected);
      selected.has(action.id) ? selected.delete(action.id) : selected.add(action.id);
      return { ...state, selected };
    }
  }
}

const MemoRow = memo(Row);

export function SearchScreen({ items }: { items: Item[] }) {
  const [state, dispatch] = useReducer(reducer, { query: '', selected: new Set<string>() });
  const query = useDebounced(state.query.trim().toLowerCase());

  const visible = useMemo(
    () => (query ? items.filter((item) => item.title.toLowerCase().includes(query)) : items),
    [items, query],
  );

  const renderItem = useCallback(
    ({ item }: { item: Item }) => (
      <MemoRow title={item.title} onPress={() => dispatch({ type: 'toggle', id: item.id })} />
    ),
    [],
  );

  return (
    <View style={styles.screen}>
      <TextInput
        placeholder="Search"
        value={state.query}
        onChangeText={(text) => dispatch({ type: 'query', query: text })}
        style={styles.search}
      />
      <FlatList data={visible} keyExtractor={(item) => item.id} renderItem={renderItem} initialNumToRender={20} />
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, padding: 16 },
  search: { borderWidth: 1, borderColor: '#d0d7de', borderRadius: 6, padding: 12, marginBottom: 12 },
});
```

## One setting

The keyboard should not cover the list on Android:

```json title="app.json"
{
  "expo": {
    "name": "Searchable List",
    "android": { "softwareKeyboardLayoutMode": "pan" }
  }
}
```

That's it: a hook, a reducer and a memoized row. The [React Native docs on FlatList](https://reactnative.dev/docs/flatlist) go further.]]></content:encoded>
            <author>rick@reactnative.land (React Native Land)</author>
        </item>
        <item>
            <title><![CDATA[Uma lista pesquisável em React Native]]></title>
            <link>https://reactnative.land/pt/posts/a-searchable-list</link>
            <guid isPermaLink="false">https://reactnative.land/posts/a-searchable-list.pt-br</guid>
            <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Quase todo app tem uma lista em que dá para pesquisar. Vamos construir uma em etapas, do menor componente até uma lista que continua rápida com milhares de linhas.  ## Comece pequeno  Uma linha é um `]]></description>
            <content:encoded><![CDATA[Quase todo app tem uma lista em que dá para pesquisar. Vamos construir uma em etapas, do menor componente até uma lista que continua rápida com milhares de linhas.

## Comece pequeno

Uma linha é um `Text` dentro de um `Pressable`:

```tsx title="Row.tsx"
import { Pressable, Text } from 'react-native';

export function Row({ title, onPress }: { title: string; onPress: () => void }) {
  return (
    <Pressable onPress={onPress}>
      <Text>{title}</Text>
    </Pressable>
  );
}
```

Primeiro, crie o app com o Expo:

```sh
npx create-expo-app@latest searchable-list
cd searchable-list && npx expo start
```

## Espere a digitação parar

Filtrar a cada tecla é desperdício. Um hook genérico devolve o valor só depois que ele fica parado por um instante:

```ts title="useDebounced.ts"
import { useEffect, useState } from 'react';

export function useDebounced<T>(value: T, delay = 250): T {
  const [settled, setSettled] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setSettled(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return settled;
}
```

## A lista

A tela guarda seu estado num reducer, filtra com `useMemo` e desenha com uma `FlatList` de linhas memoizadas, então digitar só redesenha o que mudou:

```tsx title="SearchScreen.tsx"
import { memo, useCallback, useMemo, useReducer } from 'react';
import { FlatList, StyleSheet, TextInput, View } from 'react-native';
import { Row } from './Row';
import { useDebounced } from './useDebounced';

type Item = { id: string; title: string };

type State = { query: string; selected: Set<string> };
type Action =
  | { type: 'query'; query: string }
  | { type: 'toggle'; id: string };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'query':
      return { ...state, query: action.query };
    case 'toggle': {
      const selected = new Set(state.selected);
      selected.has(action.id) ? selected.delete(action.id) : selected.add(action.id);
      return { ...state, selected };
    }
  }
}

const MemoRow = memo(Row);

export function SearchScreen({ items }: { items: Item[] }) {
  const [state, dispatch] = useReducer(reducer, { query: '', selected: new Set<string>() });
  const query = useDebounced(state.query.trim().toLowerCase());

  const visible = useMemo(
    () => (query ? items.filter((item) => item.title.toLowerCase().includes(query)) : items),
    [items, query],
  );

  const renderItem = useCallback(
    ({ item }: { item: Item }) => (
      <MemoRow title={item.title} onPress={() => dispatch({ type: 'toggle', id: item.id })} />
    ),
    [],
  );

  return (
    <View style={styles.screen}>
      <TextInput
        placeholder="Pesquisar"
        value={state.query}
        onChangeText={(text) => dispatch({ type: 'query', query: text })}
        style={styles.search}
      />
      <FlatList data={visible} keyExtractor={(item) => item.id} renderItem={renderItem} initialNumToRender={20} />
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, padding: 16 },
  search: { borderWidth: 1, borderColor: '#d0d7de', borderRadius: 6, padding: 12, marginBottom: 12 },
});
```

## Uma configuração

No Android, o teclado não deve cobrir a lista:

```json title="app.json"
{
  "expo": {
    "name": "Searchable List",
    "android": { "softwareKeyboardLayoutMode": "pan" }
  }
}
```

Pronto: um hook, um reducer e uma linha memoizada. A [documentação do React Native sobre FlatList](https://reactnative.dev/docs/flatlist) vai além.]]></content:encoded>
            <author>rick@reactnative.land (React Native Land)</author>
        </item>
        <item>
            <title><![CDATA[A blog]]></title>
            <link>https://reactnative.land/posts/a-blog</link>
            <guid isPermaLink="false">https://reactnative.land/posts/a-blog.en</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Welcome to React Native Land!  I hope I can write about React Native development here!  _Stay tuned!_]]></description>
            <content:encoded><![CDATA[Welcome to React Native Land!

I hope I can write about React Native development here!

_Stay tuned!_]]></content:encoded>
            <author>rick@reactnative.land (React Native Land)</author>
        </item>
        <item>
            <title><![CDATA[Um blog]]></title>
            <link>https://reactnative.land/pt/posts/a-blog</link>
            <guid isPermaLink="false">https://reactnative.land/posts/a-blog.pt-br</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Bem-vindo ao React Native Land!  Espero poder escrever sobre React Native aqui!  _Fique ligado!_]]></description>
            <content:encoded><![CDATA[Bem-vindo ao React Native Land!

Espero poder escrever sobre React Native aqui!

_Fique ligado!_]]></content:encoded>
            <author>rick@reactnative.land (React Native Land)</author>
        </item>
    </channel>
</rss>
