Rork で個人アプリを出したあと、アクセス解析だけ眺めていると気付かないけれど、レビューを丁寧に読むと分かることがあります。「画面が真っ白のまま動かない」「ボタンを押しても何も起きない」という声が、思ったより多いのです。
こちらとしては「通信エラーが出たら再試行してください」と言いたくなるのですが、ユーザーから見ると、押したのに何も反応しない時点でアプリは壊れています。私は壁紙系と癒し系のアプリを長く運用するなかで、このギャップを埋めるためだけに UI を何度か作り直してきました。この記事は、その試行錯誤のなかで手元に残った設計指針のまとめです。
「なにもない画面」が星を削る
体感で一番多いクレームは、通信が遅いタイミングで画面が何も出ない状態です。スピナーすら出ない場合もあり、ユーザーは「フリーズした」「バグってる」と判断します。Rork で SDK 周りを使っていると、失敗の通知が非同期で遅れて届くため、ローディングとエラーの境目が曖昧になりがちです。
まず徹底すべきは、初回描画のタイミングで必ず何かを出すことです。スケルトン UI でも、プレースホルダのイラストでも構いません。「読み込み中」を視覚的に宣言するだけで、体感の壊れ方が大きく変わります。
// Rork + React Native 想定
export function FeedScreen() {
const { data, isLoading, error, refetch } = useFeed();
if (isLoading && !data) return <FeedSkeleton />;
if (error && !data) return <FeedErrorEmpty onRetry={refetch} />;
if (!data || data.length === 0) return <FeedEmpty onRetry={refetch} />;
return <FeedList data={data} onRetry={refetch} />;
}ポイントは、isLoading && !data と error && !data と data?.length === 0 の 3 つを別の画面として描くことです。「ロード中か?」だけで出し分けると、再読み込み時にちらついて空配列が一瞬見えたりして、それだけで体験が荒れます。
私がやっている細かい工夫としては、isLoading がずっと true のまま 8 秒を超えたら、ローディング画面内にそっと「通信が遅いようです。接続状況をご確認ください」と一文出すことです。進まない時間を、アプリの無言ではなくメッセージで埋めるだけで、レビューに書かれる「フリーズした」がだいぶ減ります。
ボタンの「反応ゼロ」をなくす
次に削れるのが、押したのに何も起きないボタンです。送信ボタンを押した瞬間から、サーバー応答が返ってくるまでの間に、UI が何も変化しないパターンがこれに当たります。
最低限、押した直後にボタンの見た目を変えることと、disabled を即時に入れることは徹底します。二重タップ防止の意味でも必須です。
export function SubscribeButton({ onSubscribe }: Props) {
const [state, setState] = useState<"idle" | "loading" | "success" | "error">("idle");
const handlePress = async () => {
if (state === "loading") return;
setState("loading");
try {
await onSubscribe();
setState("success");
} catch (e) {
setState("error");
}
};
return (
<Button
disabled={state === "loading"}
onPress={handlePress}
variant={state === "error" ? "danger" : "primary"}
>
{state === "loading" ? "送信中…" : state === "success" ? "送信しました" : state === "error" ? "送信できませんでした。もう一度お試しください" : "登録する"}
</Button>
);
}ポイントは idle / loading / success / error の 4 状態を明示的に持つことです。この状態機械を最初に書くだけで、「押したのに何も起きない」はほぼ消えます。
もう 1 点、error に入ったときに、すぐ idle に戻さずボタン自体で「失敗したよ」と主張させるのが実務的には効きます。トースト通知を出すだけだと、ユーザーがスクロールしていると気付かれません。失敗の情報は、ユーザーの視線の位置に出すのが基本です。
不安定な通信を「飲み込む」再送キュー
UI が整ったあとに必要になるのが、失敗したリクエストを静かに飲み込む仕組みです。決済のような致命的な操作は別として、いいね・コメント・設定の保存・簡単なログ送信など、失敗しても致命的でないが、落ちるとユーザー体験を荒らす操作は、再送キューで吸収します。
最小構成はシンプルです。キューはメモリと AsyncStorage の二段構えにしておきます。
type QueuedOp = {
id: string;
type: "like" | "settings_save" | "log";
payload: unknown;
createdAt: number;
attempts: number;
};
async function enqueue(op: QueuedOp) {
const queue = await readQueue();
queue.push(op);
await writeQueue(queue);
tryFlush();
}
async function tryFlush() {
const queue = await readQueue();
if (queue.length === 0) return;
const next = queue[0];
try {
await sendOp(next);
queue.shift();
await writeQueue(queue);
tryFlush();
} catch {
const updated = [{ ...next, attempts: next.attempts + 1 }, ...queue.slice(1)];
await writeQueue(updated);
// 次のネットワーク回復時 or 指数バックオフで再試行
}
}この実装で大切なのは、enqueue した時点で UI にはすぐ成功として反映することです。見た目上のレスポンスが即時になるので、通信の遅さが体感から消えます。もちろん、最終的に送信に失敗する可能性はあるので、キューが長時間解消しない場合は、そっとバナーで「同期が滞っています」と表示するなどのフォールバックを用意します。
もう 1 つの注意点は、操作の冪等性です。同じ id のリクエストを 2 回送っても問題ないようサーバー側を設計しておくと、再送キューが安心して動けます。私は個人アプリでも、POST のボディに client_op_id を必ず含める規約にしています。サーバー側は直近一定期間の client_op_id を見て、重複は静かに捨てます。
「低速通信中」を文言で伝える
UX のチューニングで、意外なほど効くのが文言です。「読み込めませんでした」だけ表示するアプリと、「通信が不安定なようです。電波のよい場所で再度お試しください」と表示するアプリでは、ストアレビューの荒れ方が明確に違います。
私が使い回している表現を、いくつか共有しておきます。
- 低速で読み込み中: 「通信が少し遅いようです。もうしばらくお待ちください」
- 読み込み失敗(自動再試行中): 「うまく取得できませんでした。自動で再試行しています…」
- 読み込み失敗(手動再試行): 「うまく取得できませんでした。電波の状況を確認のうえ、もう一度お試しいただけますか?」
- オフライン時: 「インターネットに接続されていないようです。接続が戻ったら自動で再取得します」
「再試行」という技術用語を避け、ユーザーに何をしてほしいかを一文でお願いする形にしているのがコツです。「押してください」ではなく「お試しいただけますか?」のような柔らかい問いかけのほうが、レビュー欄での印象はかなり違います。
英語版でも同じ方針で、「Please try again in a moment. Your last action will sync automatically once the connection returns.」のように、ユーザーが何もしなくても復帰することを明記します。操作の責任をユーザーだけに押し付けない書き方です。
オフライン時に壊してはいけない機能
最後に、オフライン時でも最低限維持したい機能を洗い出す話をしておきます。個人アプリでも、全機能をオフライン対応する必要はありません。ただし、オフラインでも動くべき機能が動かないと、ユーザーは「このアプリは壊れやすい」と判断します。
私が自分の壁紙アプリで決めているルールはこの 3 点です。
- すでにダウンロード済みの壁紙は、オフラインでもいつも通り表示・設定できる
- 設定画面(テーマ切り替え、言語設定など)はオフラインでも動く
- お気に入り登録は、オフラインでも UI 上はできて、あとから同期される
逆に、ランキング表示や新着更新など、ネットワークが前提の画面は、オフライン時にそれとわかる空状態を出すだけに留めます。ここを分けて設計するだけで、電波のない電車の中で触られても「壊れている感」が減ります。
ネットワークの悪い日を、アプリ品質のテスト日にする
Rork で作ったアプリをもう一段上のクオリティに持っていきたいなら、わざと電波の悪い状態で自分のアプリを触る日を作るのが近道です。iOS の「通信状況をシミュレート」や Android Studio の Network Throttling を使って、3G 相当や Offline で 15 分ほど操作してみると、普段気付かない穴がたくさん見つかります。
私はリリース前に、この「低速通信チェック」を必ず通しています。ダミーのサーバー遅延を挟んで、全画面を順に触り、ローディング・空状態・エラー・再試行がすべて気持ちよく動くかを確認します。ここでクレームの 7 〜 8 割は潰せている実感があります。
個人で運用していると、機能追加の優先度が上がりがちで、こうした地味な UX 改修は後回しにしがちです。それでも、このカテゴリの改善は星 4.0 と 4.5 の間を分けることが多く、長期的に見れば広告収益や課金率にも効いてきます。電波の悪い日を敵ではなく、自分のアプリの弱点を教えてくれるコーチとして扱ってみてください。