THE FINNTERNET

Adding tvOS & Android TV Support to Monorepo

9/27/2021

TL;DR

I was in the process of creating a react-native monorepo when I stumbled upon this awesome repo that Matteo Mazzarolo is developing, so I decided to build off of that. His repo already supports iOS, Android, macOS, Windows, general desktop using Electron, web, and a browser extension, so I figured let's round it out with some TV support.

Initial Setup

First we will clone and setup the monorepo.

% git clone :thefinnomenon/react-native-universal-monorepo.git
% cd react-native-universal-monorepo
% git checkout 06aafb1202c55f1a5d6625a54947334936558b33
% yarn

Add Scripts to Root

Now we can add some scripts for interacting with our TV project when we finish it.

// package.json
{
  ...
  "scripts": {
    ...
    "tv:android:metro": "yarn workspace @my-app/tv start",
    "tv:android:start": "yarn workspace @my-app/tv android",
    "tv:android:studio": "yarn workspace @my-app/tv studio",
    "tv:tvos:metro": "yarn workspace @my-app/tv start",
    "tv:tvos:start": "yarn workspace @my-app/tv ios",
    "tv:tvos:xcode": "yarn workspace @my-app/tv xcode",
    "tv:tvos:pods": "yarn workspace @my-app/tv pods",
    ...
  }
  ...
}

Initialize TV App

Let's use react-native-template-typescript-tv as a template to create our app.

% cd packages
% npx react-native init tv --template react-native-template-typescript-tv
% cd tv

Update package.json

Next rename the package to match the naming scheme of the project, add some helpful scripts, and add our app package and async-storage dependencies.

// packages/tv/package.json
{
  "name": "@my-app/tv",
  ...
  "scripts": {
    ...
    "android": "emulator -avd Android_TV; react-native run-android",
    "ios": "react-native run-ios --simulator 'Apple TV' --scheme 'tv-tvOS'",
    "studio": "studio android",
    "xcode": "xed ios",
    "pods": "pod-install",
    ...
  },
  "dependencies": {
    "@my-app/app": "*",
    "@react-native-async-storage/async-storage": "^1.15.7",
    ...
  },
  ...
}
% yarn add -D pod-install
# Since aync-storage has a native component, we need to run this too
% yarn pods

If you get an lstat error just rerun the command.

Modify App

Now we can replace the default App with the one from our app package.

// packages/tv/index.js
import {AppRegistry} from 'react-native';
import {App} from '@my-app/app';
import {name as appName} from './app.json';

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

Add Monorepo Support

If we run the app right now, it won't be able to resolve any of the hoisted dependencies so we have to configure it to work with the monorepo.

% yarn add -D react-native-monorepo-tools

If you get an lstat error just rerun the command.

// packages/tv/metro.config.js
const exclusionList = require("metro-config/src/defaults/exclusionList");
const { getMetroTools, getMetroAndroidAssetsResolutionFix } = require("react-native-monorepo-tools");

const monorepoMetroTools = getMetroTools();

const androidAssetsResolutionFix = getMetroAndroidAssetsResolutionFix();

module.exports = {
  transformer: {
    // Apply the Android assets resolution fix to the public path...
    publicPath: androidAssetsResolutionFix.publicPath,
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },
  server: {
    // ...and to the server middleware.
    enhanceMiddleware: (middleware) => {
      return androidAssetsResolutionFix.applyMiddleware(middleware);
    },
  },
  // Add additional Yarn workspace package roots to the module map.
  // This allows importing importing from all the project's packages.
  watchFolders: monorepoMetroTools.watchFolders,
  resolver: {
    // Ensure we resolve nohoist libraries from this directory.
    blockList: exclusionList(monorepoMetroTools.blockList),
    extraNodeModules: monorepoMetroTools.extraNodeModules,
  },
};

Update config.ts for TV Subplatforms

We have to update the platform detection code in config.ts to handle our new subplatforms.

// packages/app/src/config.ts
import { Platform } from 'react-native';

// Import TV specific types (uncomment if developing for TV platform)
// import '../../tv/node_modules/react-native/tvos-types.d';

// eslint-disable-next-line
declare var __SUBPLATFORM__: "electron" | "browser-ext" | "android-tv" | "tvos" | undefined;

export const isDev = __DEV__;

// Tells on what variant of the platform we're running
export let subplatform: typeof __SUBPLATFORM__ = undefined;

// Injected in electron and browser-extension builds.
if (typeof __SUBPLATFORM__ === "string") {
  subplatform = __SUBPLATFORM__
}
// For tvOS and Android TV, we can check the Platform.isTV field 
else if (Platform.isTV && Platform.OS === "ios") {
  subplatform = "tvos";
} else if (Platform.isTV && Platform.OS === "android") {
  subplatform = "android-tv";
}

Create Android TV Virtual Device

  1. Open SDK Manager and install the Android TV Intel x86 Atom System Image for your current SDK.
  2. Open AVD Manager and create a TV device using the system image you just installed (Name it Android TV).

Bump Android minSDKVersion

When I tried to build I got an error because the minSDKVersion was 16 and a library was requiring a minimum of 21 so we can bump it to 21.

// packages/tv/android/app/build.gradle
...
android {
  ...
  defaultConfig {
    ...
    minSdkVersion 21
    ...
  }
  ...
}
...

Run

From root directory,

% yarn tv:tvos:start
% yarn tv:android:start

You will see a warning, "Persistent storage is not supported on tvOS...". You can ignore this since we are simply using async-storage as an example, but if you want to actually persist data for a tvOS app you will have to find another solution.

Test Other Platforms

Now that we have our TV platforms working, we need to check to make sure the other platforms still work.

For web, browser-ext, & electron I get this error,

The react-scripts package provided by Create React App requires a dependency:

  "jest": "26.6.0"

Don't try to install it manually: your package manager does it automatically.
However, a different version of jest was detected higher up in the tree:

  .../react-native-universal-monorepo/node_modules/jest (version: 26.6.3)

Manually installing incompatible versions is known to cause hard-to-debug issues.

If you would prefer to ignore this check, add SKIP_PREFLIGHT_CHECK=true to an .env file in your project.
That will permanently disable this message but you might encounter other issues.

This is because Jest is being hoisted but it shouldn't be an issue, so just follow the instructions to ignore the check by adding a .env file with SKIP_PREFLIGHT_CHECK=true to each of those project directories.

For macos I ran into a open file limit which I solved by installing watchman,

% brew update
% brew install watchman

All the other platforms ran fine without modifications šŸ‘šŸ½.

Final Thoughts

It's really cool that react-native enables developers to write an app that runs on multiple platforms with minimal extra work. Having a monorepo with all the platforms already setup is a huge timesaver and now having TV supported, we can easily throw the new dancing cat app we have been working on up on the big screen so our family can pretend to be proud of us while they wonder why we couldn't have been a talented doctor like our cousin Greg.

Chris' bitmoji smiling

Hi, how's it going? Iā€™m Chris Finn, a full-stack developer from Boston, MA. I specialize in UI and UX design, cross-platform app development, and eating calzone. I am also a sucker for automation and security (in a past life I was a cybersecurity researcher).