【コピペでOK】React NativeにAndroid用ウィジェットを実装する

Featured image of the post

はじめに

React Nativeで一からウィジェットを作ろうとすると、ネイティブコードが必要で大変💦

そこでこの記事では、ネイティブコードを書かずに簡単にAndroid用のウィジェットを実装する方法をまとめる😊

💡
今回はExpoを使った開発を前提に解説するが、ExpoなしでもOK!

作るもの

「Hello」と表示するだけの最小限のウィジェットを作成する。

Image in a image block

結論

✅パッケージ「React Native Android Widget」を使う。

TypeScript(JavaScript)のファイルを編集するだけで実装できる。

React Native Android Widgetって何?

Androidのウィジェットを簡単に作れるパッケージ。

iOSは非対応?

Androidのみ対応。

(iOSは別途作成が必要。今回はAndroidのみ解説する。)

iOS用のパッケージもいくつか存在する。

最小限のウィジェットの作り方

以下のとおり順番に真似すれば最小限のウィジェットが作れる😊

準備(使用する手法・技術)

事前に用意しておくもの

  • React Native(Expo)のプロジェクト

今からインストールするもの

  • React Native Android Widget

【手順1】React Native Android Widgetをインストールする
npm install --save react-native-android-widget

【手順2】ウィジェットのコンポーネントを作成する

「Hello」と表示するコンポーネントを作成する。

components/widget/HelloWidget.tsx

(ファイルの場所はどこでもOK。コピペでOK。)

import React from 'react';
// ウィジェット専用のコンポーネント
import { FlexWidget, TextWidget } from 'react-native-android-widget';

export function HelloWidget() {
  return (
    <FlexWidget
      style={{
        height: 'match_parent',
        width: 'match_parent',
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: '#ffffff',
        borderRadius: 16,
      }}
    >
      <TextWidget
        text="Hello"
        style={{
          fontSize: 32,
          fontFamily: 'Inter',
          color: '#000000',
        }}
      />
    </FlexWidget>
  );
}

【解説】ウィジェット専用のコンポーネント
// ウィジェット専用のコンポーネント
import { FlexWidget, TextWidget } from 'react-native-android-widget';

公式ドキュメントに記載されているウィジェット専用のコンポーネントを使う必要がある!

【手順3】タスクハンドラーを作成する

✅ウィジェットにはタスクハンドラーが必須。

【補足】タスクハンドラーとは

主に2つの役割をするファイル。

  • ウィジェットをホーム画面に追加/更新するロジック
  • ウィジェットのクリック処理

components/widget/widget-task-handler.tsx

(ファイルの場所はどこでもOK。コピペでOK。)

import React from 'react';
import type { WidgetTaskHandlerProps } from 'react-native-android-widget';
import { HelloWidget } from './HelloWidget';

// ①ウィジェットの名前 と コンポーネント の紐づけ
const nameToWidget = {
  Hello: HelloWidget,
};

export async function widgetTaskHandler(props: WidgetTaskHandlerProps) {
  const widgetInfo = props.widgetInfo;
  const Widget =
    nameToWidget[widgetInfo.widgetName as keyof typeof nameToWidget];

	// ②イベント処理
  switch (props.widgetAction) {
    case 'WIDGET_ADDED':
      props.renderWidget(<Widget />);
      break;

    case 'WIDGET_UPDATE':
      // Not needed for now
      break;

    case 'WIDGET_RESIZED':
      // Not needed for now
      break;

    case 'WIDGET_DELETED':
      // Not needed for now
      break;

    case 'WIDGET_CLICK':
      // Not needed for now
      break;

    default:
      break;
  }
}

【解説】①ウィジェットの名前 と コンポーネント の紐づけ
// ①ウィジェットの名前 と コンポーネント の紐づけ
const nameToWidget = {
  Hello: HelloWidget,
};

先ほど作ったコンポーネント「HelloWidget」を「Hello」というウィジェット名に設定する。

【解説】②イベント処理
// ②イベント処理
switch (props.widgetAction) {
  case 'WIDGET_ADDED':
    props.renderWidget(<Widget />);
    break;

  case 'WIDGET_UPDATE':
    // Not needed for now
    break;

  case 'WIDGET_RESIZED':
    // Not needed for now
    break;

  case 'WIDGET_DELETED':
    // Not needed for now
    break;

  case 'WIDGET_CLICK':
    // Not needed for now
    break;

  default:
    break;
}

ウィジェットを追加/更新/リサイズ/削除/クリックしたときの処理を定義する。

【手順4】タスクハンドラーを登録する

先ほど作ったタスクハンドラーを登録する。

  1. エントリーポイントをindex.tsに変更する。

    package.json

    {
      "name": "my-expo-app",
      "main": "index.ts",       // これ
      ...
    }

  2. 空のindex.tsファイルを作成する。

  3. 一旦、デフォルトのエントリーポイント「node_modules/expo/AppEntry.js」をコピペする。

    index.ts

    import registerRootComponent from 'expo/build/launch/registerRootComponent';
    
    // パスは変更する
    import App from '.App';
    
    registerRootComponent(App);

  4. タスクハンドラーを登録する処理を追記する。

    index.ts

    import registerRootComponent from 'expo/build/launch/registerRootComponent';
    // ✅追記
    import { registerWidgetTaskHandler } from 'react-native-android-widget';
    
    import App from './App';
    // ✅追記
    import { widgetTaskHandler } from './widget-task-handler';
    
    registerRootComponent(App);
    // ✅追記
    registerWidgetTaskHandler(widgetTaskHandler);

【補足】素のReact Nativeの場合

基本は上記と同じ。

index.tsの内容だけ以下に変更する。

import { AppRegistry } from 'react-native';
import { registerWidgetTaskHandler } from 'react-native-android-widget';
import { name as appName } from './app.json';
import App from './App';
import { widgetTaskHandler } from './widget-task-handler';

AppRegistry.registerComponent(appName, () => App);
registerWidgetTaskHandler(widgetTaskHandler);

【補足】Expo Routerを使用している場合

基本は上記と同じ。

index.tsの内容だけ以下に変更する。

(node_modules/expo-router/entry.jsを元にして、registerWidgetTaskHandlerを追記する。)

// `@expo/metro-runtime` MUST be the first import to ensure Fast Refresh works
// on web.
import '@expo/metro-runtime';

import { App } from 'expo-router/build/qualified-entry';
import { renderRootComponent } from 'expo-router/build/renderRootComponent';

import { registerWidgetTaskHandler } from 'react-native-android-widget';
import { widgetTaskHandler } from '@/components/widget/widget-task-handler';

// This file should only import and register the root. No components or exports
// should be added here.
renderRootComponent(App);
registerWidgetTaskHandler(widgetTaskHandler);

【手順5】ウィジェットを登録する

✅config pluginがサポートされているので、app.config.tsにウィジェットの情報を登録するだけでOK。

app.config.ts

import type { ConfigContext, ExpoConfig } from 'expo/config';
import type { WithAndroidWidgetsParams } from 'react-native-android-widget';

const widgetConfig: WithAndroidWidgetsParams = {
  // 【任意】カスタムフォントのパス
  fonts: ['./assets/fonts/Inter.ttf'],
  widgets: [
    {
      name: 'Hello', // ウィジェットの名前(タスクハンドラーに書いたウィジェット名に合わせる)
      label: 'My Hello Widget', // ウィジェットピッカーに表示されるラベル
      minWidth: '320dp',
      minHeight: '120dp',
      // これは、ウィジェットのデフォルトのサイズがtargetCellWidthとtargetCellHeight属性で指定された5x2セルであることを意味します。
      // または、Android 11以下のデバイスではminWidthとminHeightで指定された320×120dpであることを意味します。
      // 定義されている場合、targetCellWidthとtargetCellHeightの属性はminWidthやminHeightの代わりに使用されます。
      targetCellWidth: 5,
      targetCellHeight: 2,
      description: 'This is my first widget', // ウィジェットピッカーに表示される説明
      previewImage: './assets/widget-preview/hello.png', // 【任意】ウィジェットプレビュー画像へのパス

      // このAppWidgetが更新される頻度(ミリ秒単位)。
      // タスクハンドラはwidgetAction = 'UPDATE_WIDGET'で呼び出されます。
      // デフォルトは0(自動更新なし)
      // 最小は1800000(30分 == 30 * 60 * 1000)。
      updatePeriodMillis: 1800000,
    },
  ],
};

export default ({ config }: ConfigContext): ExpoConfig => ({
  ...config,
  name: 'My Expo App Name',
  plugins: [['react-native-android-widget', widgetConfig]],
});

【補足】素のReact Nativeの場合

【手順6】実行する

✅Expo Goでは実行できないため、開発ビルドで動作確認する。

  1. 開発ビルドのビルド設定を追加

    eas.json

    {	
      "build": {
    	  ...
      
    	  // 追記
        "development": {
          "developmentClient": true,
          "distribution": "internal",
          "android": {
            "buildType": "apk"
          }
        },
        
        ...
      },
    }
    

  2. 開発ビルドで必要なパッケージ「expo-dev-client」をインストール
    npx expo install expo-dev-client

  3. 開発ビルドを実行

    ※EASのアカウントが必要(無料)

    eas build --platform android --profile development

  4. ビルドが完成したら実機またはエミュレータにインストールする。

    (ビルド結果のWebページからapkファイルをダウンロードすればOK)

    Image in a image block
    Image in a image block

  5. 動作確認

    開発PCでnpx expo start --dev-clientを実行した状態で、実機またはエミュレータでウィジェットが追加できるか動作確認する。

    Image in a image block

【補足】素のReact Nativeの場合

普通にアプリを再ビルドして実機またはエミュレータにインストールすればOK。

まとめ

React Native Android Widgetを使って、ネイティブコードを書かずにウィジェットを作ることができた✨

ポイント
  • 見た目やロジックをJavaScriptで書くことができる。
  • ネイティブコードを知らなくてもウィジェットが作れる。
  • 順番にコピペするだけで動作するので敷居が低い。

またウィジェットの設定画面の作成などもできるので、実用的なウィジェットも作成できそう😊

参考サイト