私が運用している壁紙アプリのレビューに、あるとき「Wi-Fi のないところで開いたら、あっという間にギガが減った」という指摘が届きました。原因はすぐに分かりました。一覧画面で次のページの高解像度画像を片っ端から先読みしていたのです。Wi-Fi なら快適さに貢献する挙動が、従量制のモバイル回線では利用者の通信量を静かに食いつぶしていました。画像を多く扱うアプリほど、この「良かれと思った先読み」が逆効果になりやすいと感じています。
ここでは Rork で生成した Expo アプリを題材に、回線の性質を読み取って先読みと画質を適応的に切り替える通信設計を整理します。鍵になるのは、回線を「つながっているか/いないか」の二値で見るのをやめ、「従量制か」「低速か」「低データモードか」まで踏み込んで判断することです。
「オンラインか否か」だけで判断するのをやめる
多くのアプリは isConnected だけを見て、つながっていれば全部やる、という作りになっています。しかし利用者の回線は一様ではありません。NetInfo の details には、判断に使える情報がもっと含まれています。
| 判定軸 | 取得元(NetInfo) | 意味するもの |
|---|---|---|
| 従量制かどうか | details.isConnectionExpensive | OS が「お金のかかる回線」と見なしている(テザリング等を含む) |
| 回線種別 | type(wifi / cellular / ...) | Wi-Fi かモバイルか |
| セルラー世代 | details.cellularGeneration(3g/4g/5g) | 速度の目安(3g なら重い先読みは避ける) |
| 低データモード | 下記参照 | 利用者が明示的に通信を絞っている |
isConnectionExpensive は OS が判断した「従量制相当」のフラグで、テザリング経由の Wi-Fi なども含まれます。これを尊重するだけでも、レビューにあったような「ギガが溶ける」事故の多くは防げます。
低データモードについては補足が要ります。iOS の Low Data Mode / Android のデータセーバーは、アプリから直接「オンかどうか」を読む統一 API が乏しいのが実情です。実務では、isConnectionExpensive を低データモードの代理シグナルとして扱い、加えて「アプリ内に節約スイッチを用意して利用者自身に選ばせる」二段構えにするのが現実的です。OS の意思(従量制判定)とアプリの設定(明示スイッチ)を両方ポリシーに合流させます。
通信判定を一箇所に集約する
回線の判定を画面ごとに散らすと、必ず判断がばらつきます。回線情報を1つのポリシーに変換するレイヤーを作り、各画面はそのポリシーだけを見るようにします。
// net/networkPolicy.ts
import NetInfo, { NetInfoState } from "@react-native-community/netinfo";
export type PrefetchLevel = "full" | "lite" | "off";
export type ImageQuality = "high" | "medium" | "low";
export interface NetworkPolicy {
prefetch: PrefetchLevel; // 先読みの強さ
imageQuality: ImageQuality; // 取得する画質
allowAutoplay: boolean; // 動画・GIF の自動再生可否
}
// 回線状態 + アプリの節約設定 を 1 つのポリシーへ変換する
export function derivePolicy(
state: NetInfoState,
saverEnabled: boolean
): NetworkPolicy {
// つながっていない
if (!state.isConnected) {
return { prefetch: "off", imageQuality: "low", allowAutoplay: false };
}
const expensive = !!(state.details as any)?.isConnectionExpensive;
const gen = (state.details as any)?.cellularGeneration as string | undefined;
const isSlowCellular = state.type === "cellular" && (gen === "2g" || gen === "3g");
// 利用者が節約を選んでいる、または OS が従量制と判断、または低速
if (saverEnabled || expensive || isSlowCellular) {
return { prefetch: "off", imageQuality: "medium", allowAutoplay: false };
}
// 通常のモバイル(4g/5g・非従量制)
if (state.type === "cellular") {
return { prefetch: "lite", imageQuality: "high", allowAutoplay: false };
}
// Wi-Fi(非従量制)— 全力で先読み
return { prefetch: "full", imageQuality: "high", allowAutoplay: true };
}このポリシーを React のコンテキストで配ると、各画面は useNetworkPolicy() を読むだけで一貫した判断ができます。
// net/NetworkPolicyProvider.tsx
import React, { createContext, useContext, useEffect, useState } from "react";
import NetInfo from "@react-native-community/netinfo";
import { derivePolicy, NetworkPolicy } from "./networkPolicy";
import { useDataSaver } from "../settings/useDataSaver"; // アプリ内の節約スイッチ
const Ctx = createContext<NetworkPolicy>({
prefetch: "lite", imageQuality: "high", allowAutoplay: false,
});
export function NetworkPolicyProvider({ children }: { children: React.ReactNode }) {
const saver = useDataSaver();
const [policy, setPolicy] = useState<NetworkPolicy>(Ctx._currentValue);
useEffect(() => {
// 購読開始時に現在値を一度取得し、以後は変化を購読
const unsub = NetInfo.addEventListener((state) => {
setPolicy(derivePolicy(state, saver));
});
NetInfo.fetch().then((state) => setPolicy(derivePolicy(state, saver)));
return () => unsub();
}, [saver]);
return <Ctx.Provider value={policy}>{children}</Ctx.Provider>;
}
export const useNetworkPolicy = () => useContext(Ctx);ポイントは、節約スイッチ(saver)の変化も依存に入れることです。利用者が設定でスイッチを切り替えた瞬間にポリシーが再計算され、先読みの挙動が即座に追従します。