👻

React Nativeアプリ開発の教科書: Blueskyクライアント"Graysky"のコードを読む

2024/04/14に公開

React Nativeアプリ開発の教科書のようなソースコード

Grayskyは、iOS/Androidに対応したサード・パーティー製のBlueskyクライアントです。
Blueskyに書き込んだり、フォロー中のユーザーの書き込みやフィードを読む機能を提供していて動作も軽快なので、利用しているBlueskyユーザーも多いのではないでしょうか。

https://graysky.app/

ネイティブ・アプリとして快適な使用感を達成しているこのアプリケーション、React Nativeをベースに開発されていてGithubでソース・コードも公開されています。

https://github.com/mozzius/graysky

チェックしたところ、アプリケーションを開発したいエンジニアにとって「教科書」となりそうな内容だったので、こちらで紹介してみます。

使われている主なパッケージ

フロントエンド

  • Expo SDK 50
  • Native Wind
  • React Native Bottom Sheet
  • Zeego
  • Shopify Flash List
  • Atproto API
  • Tanstack React Query
  • Lingui
  • Lucide Icon
  • React Native MMKV
  • zod

バックエンド

  • MySQL...
  • tRPC
  • Firebase
  • Next.js

注意: アクティブな開発は終了してメンテモード、開発者は本家Blueskyのメンバーに

このGraysky、開発者が本家Blueskyにjoinすることを発表していて、今後はメンテナスのみ行うとのこと。残念ではありますが「本家」もクライアント・コードをGithubで公開しているので、今後は本家が改善されていくことが期待できますね。

Big news (which no one saw coming) - I've joined Bluesky! I'll be working on the app alongside the rest of the frontend team.

This account is probably going to be a bit more work-centric from now on - I've made a new account with my old handle if you want to follow me there
Mar 12, 2024 at 7:05
https://bsky.app/profile/samuel.bsky.team/post/3knhay62c6d2z

ソースコードの読み方

この記事では、アプリ開発に共通する「ルーティング」「ステート管理」などのスタックごとにコードを読んでいます。Grayskyを使いながら「この画面はどうやって実装しているんだろう?」と思った場合は、UIのラベルでソースコードの翻訳リソース(apps/expo/src/i18n/locales/ja/messages.po)を検索すると、ソースコードのパスを見つけるのが簡単になります。

ルーティング

ルーティングにはExpo Routerを採用しています。

XXXScreenのようなスクリーン・コンポーネントを間に挟まず、Expo Routerのルート定義にそのまま画面のコードを配置しています。思い切ったレイアウトにも見えますが、スクリーン・コンポーネントのファイルレイアウトがそのままルートを表現できる利点があります。

(同じスクリーンを別なルートで使い回す場合はどうしてるのかな)

https://docs.expo.dev/router/introduction/

Expo Router自体が比較的新しい存在で、採用について悩んでいるチームもあるかと思いますが、こうした形で実働するアプリとコードがあると安心できます。ただし、GrayskyはWeb版は提供しておらず「ユニバーサル・ルータ」としてのExpo Routerの使い勝手は未知数です。

スタイリング

ビュー・コンポーネントのスタイリングには NativeWind を採用しています。

apps/expo/src/app/edit-bio.tsx
<View className="absolute h-full w-full items-center justify-center bg-black/40">
  <ImagePlusIcon color="white" size={32} />
</View>

TailWind互換のスタイリング用クラスでコンポーネントのスタイリングを直接指定しています。

https://www.nativewind.dev/

UIコンポーネント

いわゆるUIライブラリは使用しておらず、上記のNative Windを使って独自にUIコンポーネントを実装しています。

メニューについてはZeegoを採用していて、UIがネイティブっぽくなるのにも一役買っています。

https://zeego.dev/

アニメーション

フィード画面のお気に入りフィード編集画面は、ドラッグ操作で順序を入れ替えるUIや編集モードを切り替えるときのアニメーションなど、よりネイティブらしい動きのあるUIが提供されています。

ドラッグ操作できるリストは react-native-draggable-flatlist、編集モードを切り替えたときのアニメーションは react-native-reanimated で実装されています。

詳細は apps/expo/src/components/feed-row.tsx を読んでみて下さい。

https://github.com/computerjazz/react-native-draggable-flatlist
https://docs.swmansion.com/react-native-reanimated/

リスト表示

メッセージの一覧など大量なデータを扱う「リスト」については、React Native標準のFlatListではなくてShopifyが開発したFlashListが選ばれています。

https://github.com/Shopify/flash-list

Grayskyのメッセージの一覧はスクロール時のもたつきやチラツキもなく快適に動作しているのですが、FlashListが良い仕事をしているようです。

ステート管理

Reduxのような状態管理ツールは使っておらず、React.useState など標準のHooksを使ってUIコンポーネント内で状態管理をしています。

後述しますが、API経由で習得するリモートのデータはTanstackのreact-queryを使っています。

APIクライアント、サーバ・ステート・キャッシュ

APIについてはBlueskyプロジェクトが公開している @atproto/api を Tanstack React Queryでラップしています。

https://github.com/bluesky-social/atproto
https://tanstack.com/query/latest/docs/framework/react/overview

入力データのバリデーション、型チェック

バリデーションあるいは型チェックについては、サーバの実装とあわせてtRPCを使っています。

packages/api/src/router/gifs.ts
{
  ...
  tenor: createTRPCRouter({
    search: publicProcedure
      .input(
        z.object({
          query: z.string(),
          locale: z.string().optional(),
          limit: z.number().optional(),
          cursor: z.string().optional(),
        }),
      )
      .query(async ({ input }) => {
        return await fetchTenor<TenorSearchAPIResponse>("/search", {
          q: input.query,
          locale: input.locale,
          limit: input.limit,
          pos: input.cursor,
          mediafilter: "nanomp4,tinymp4,mp4,gifpreview",
        });
      }),
  ...
}

https://trpc.io/

バックエンド

プッシュ通知の送信などバックエンド処理もJavaScript/TypeScriptを使って実装しています。
前述したtTPRCを使いつつ、Next.jsのAPIルートでAPIを実装しています。

また、ランディング・サイトについてもこのNext.jsのパートに実装されています。

https://nextjs.org/

React Navigationのヘッダーのちょっと変わった書き方

コードを眺めていて気になったのは、いわゆるスクリーンコンポーネント内に <Stack.Screen /> が配置されていたことです。通常このコンポーネントは一つ上の階層のルーター定義の部分に書かれることが多く、公式ドキュメントでも見かけたことがない書き方です。

apps/expo/src/app/(auth)/sign-up.tsx
<TransparentHeaderUntilScrolled>
  <ScrollView
    className="flex-1 px-4"
    contentInsetAdjustmentBehavior="automatic"
  >
    <Stack.Screen
      options={{
        headerRight: () => (
          <Text className="text-base">
            <Trans>1 of 3</Trans>
          </Text>
        ),
       }}
    />
    <View className="my-4 flex-1">
      <Text className="mx-4 mb-1 mt-4 text-xs uppercase text-neutral-500">
        <Trans>Email</Trans>
      </Text>
  ...

React Navigationでヘッダーのボタンを定義しつつ、スクリーン内のステートにアクセスする場合、この書き方は効率良さそうですね。

公式の書き方ガイドはこちら:
https://reactnavigation.org/docs/header-buttons/#header-interaction-with-its-screen-component

まとめ

ルーティングなどの技術スタックの選定から、フォルダ構成、バックエンドも含めたシステム全体の描き方など「アプリを一本作りたい」というエンジニアにとって、様々な場面で参考になるソースコードです。

筆者は業務でReact Nativeを使ったアプリケーション開発に関わっていますが、パフォーマンスやネティブアプリらしさの表現など日々悩んでいる部分について、Grayskyは高いレベルのユーザー体験を提供していて、参考になる部分が多くありました。

この記事のように軸を持ってコードを眺めたり、あるいは実際にGrayskyクライアントを使いながら「ここはどうやって実装しているんだろう?」とチェックしてみると勉強になるのでは?と思います。

Discussion