Build a React Native anime app

An Expo app with a search screen and a player, using the AnimeDoc API through the same small server from the React tutorial so your key never ships inside the app.

What you need

  • Node.js 18+ and the Expo Go app on your phone.
  • The server.js proxy from the React tutorial, deployed somewhere your phone can reach (or on your computer's LAN IP).
  • An AnimeDoc API key on a paid plan for playback; the $1 3-Day Pass is enough to try it.
npx create-expo-app anime-mobile
cd anime-mobile
npx expo install expo-video

Why a server in between?

Anything bundled into a mobile app can be extracted, including an API key. Your app calls your server; your server adds the key. That's the same server.js from the React tutorial; its /api/stream route also handles seeking.

The app

App.js
import { useEffect, useState } from "react";
import { Button, FlatList, Image, Pressable, SafeAreaView, Text, TextInput, View } from "react-native";
import { VideoView, useVideoPlayer } from "expo-video";

const SERVER = "https://your-server.example.com"; // where server.js runs

function Player({ uri }) {
  const player = useVideoPlayer(uri, (p) => p.play());
  return <VideoView player={player} style={{ width: "100%", aspectRatio: 16 / 9 }} />;
}

function Watch({ item, onBack }) {
  const [uri, setUri] = useState(null);
  const [message, setMessage] = useState("Loading…");

  useEffect(() => {
    (async () => {
      const detail = await fetch(`${SERVER}/api/detail?detailPath=${encodeURIComponent(item.detailPath)}`).then((r) => r.json());
      const first = detail.data?.resource?.seasons?.[0] ?? { se: 0, maxEp: 0 };
      const qs = new URLSearchParams({ subjectId: item.subjectId, detailPath: item.detailPath, se: String(first.se), ep: first.maxEp ? "1" : "0" });
      const media = await fetch(`${SERVER}/api/sources?${qs}`).then((r) => r.json());
      const source = (media.data?.downloads ?? []).find((d) => d.url && !d.vipLocked);
      if (source) setUri(`${SERVER}/api/stream?url=${encodeURIComponent(source.url)}`);
      else setMessage("No playable source right now.");
    })();
  }, [item]);

  return (
    <View style={{ flex: 1, gap: 12 }}>
      <Button title="← Back" onPress={onBack} />
      <Text style={{ fontSize: 18, fontWeight: "700" }}>{item.title}</Text>
      {uri ? <Player uri={uri} /> : <Text>{message}</Text>}
    </View>
  );
}

export default function App() {
  const [q, setQ] = useState("");
  const [items, setItems] = useState([]);
  const [picked, setPicked] = useState(null);

  async function search() {
    const res = await fetch(`${SERVER}/api/search?q=${encodeURIComponent(q)}`);
    if (res.status === 429) return alert("Rate limit reached, try again soon.");
    const body = await res.json();
    setItems(body.data?.items ?? []);
  }

  return (
    <SafeAreaView style={{ flex: 1, padding: 16 }}>
      {picked ? (
        <Watch item={picked} onBack={() => setPicked(null)} />
      ) : (
        <>
          <TextInput value={q} onChangeText={setQ} onSubmitEditing={search} placeholder="Search anime" style={{ borderWidth: 1, borderColor: "#e2e8f0", borderRadius: 8, padding: 10 }} />
          <FlatList
            data={items}
            keyExtractor={(i) => i.subjectId}
            numColumns={2}
            renderItem={({ item }) => (
              <Pressable onPress={() => setPicked(item)} style={{ flex: 1, margin: 6 }}>
                <Image source={{ uri: item.cover?.url }} style={{ width: "100%", aspectRatio: 2 / 3, borderRadius: 8 }} />
                <Text numberOfLines={2}>{item.title}</Text>
              </Pressable>
            )}
          />
        </>
      )}
    </SafeAreaView>
  );
}
npx expo start

Scan the QR code with Expo Go, search, and tap a title to play its first episode.

Next steps

  • Add a season and episode picker from resource.seasons, like the React tutorial.
  • Add subtitles from captions in Media Sources.
  • Published apps with ads or purchases count as commercial use: use Pro or higher.