iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
Build on Google AI

單鐵的人生如履薄冰!AI 教練 APP 30天開發旅程,你說能走到最後嗎?系列 第 26

Day 26 | 自動部署前的細節打磨:實作深淺色模式與 API 載入鎖定防呆

  • 分享至 

  • xImage
  •  

前言:系統穩定性與體驗的最後防線

經過前面 25 天的開發,KAKERU 的動態課表推算、重新產生課表、進階智慧合併與真實日曆打卡系統功能皆完成。

在明天正式進入雲端自動化部署 (DevOps) 階段前,我們必須補齊 APP 幾項細節:

  1. 防止無效請求的「API 鎖定防呆」:呼叫 Gemini 這類 LLM API 通常需要等待數秒。若不加以限制,使用者在等待期間的連續點擊或修改輸入框,將導致前端狀態衝突並浪費後端額度。
  2. 適應多元環境的「深淺色開關」:跑者的使用情境涵蓋烈日下的戶外與夜間的室內,提供直覺的深淺色雙向開關 (Switch),並讓全域元件即時無縫響應。
  3. 版本號碼顯示:在個人設定頁面明確標註版本號碼,方便未來發布與追蹤。

動手實作 1:定義深淺色主題色票與全域 Hook

Prompt

幫加上深淺色 (Dark/Light) 模式,請幫我規劃一套好維護的架構。
我的狀態管理是用 Zustand,幫我把使用者的顏色偏好、存進去,並用 persist 持久化。
另外,我希望能有一個獨立的colors.ts
來統一管理語意化的色塊(例如背景色、卡片色、主色調等),以及一個 Custom Hook,讓我的 UI 元件可以無腦呼叫並取得目前的顏色設定。

**產出內容與程式碼,**在 src/theme/colors.ts 定義標準語意化色票,並在 src/hooks/use-color-scheme.ts 封裝全域監聽 Hook:

export const Colors = {
  light: {
    background: '#F4F5F7',
    card: '#FFFFFF',
    cardBorder: '#ECEEF2',
    text: '#1A1D20',
    secondaryText: '#6B7280',
    primary: '#FF5722',
    inputBg: '#F9FAFB',
    overlayBg: 'rgba(0, 0, 0, 0.65)',
    // ... 其他次要顏色略 ...
  },
  dark: {
    background: '#121212',
    card: '#1E1E1E',
    cardBorder: '#2D2D2D',
    text: '#F3F4F6',
    secondaryText: '#9CA3AF',
    primary: '#FF5722',
    inputBg: '#252525',
    overlayBg: 'rgba(0, 0, 0, 0.85)',
    // ... 其他次要顏色略 ...
  },
};

import { useColorScheme as useRNColorScheme } from 'react-native';
import { useStore } from '../store/useStore';
export function useColorScheme(): 'light' | 'dark' {
  const themeMode = useStore((state) => state.themeMode);
  const systemScheme = useRNColorScheme();
  if (themeMode === 'dark') return 'dark';
  if (themeMode === 'light') return 'light';
  return systemScheme === 'dark' ? 'dark' : 'light';
}

動手實作 2:全螢幕 API 載入防呆遮罩與輸入鎖定

Prompt

目前串了 Gemini API,每次生成課表大概都要等 5 到 10 秒。發現測試的時候,使用者在等待期間如果狂按送出按鈕,或是手癢去改輸入框的內容,整個 App 狀態就會大亂,甚至送出重複的 API 請求。
請幫我寫一個全螢幕的 Loading 遮罩元件(要能支援剛做好的深淺色模式),並且示範我要怎麼在打 API 的這段期間,把畫面上的輸入框和按鈕全部鎖死,直到 API 跑完才解開。

產出內容與程式碼,建立 src/components/LoadingOverlay.tsx,並在呼叫 API 的頁面中進行雙重鎖定:

import React from 'react';
import { ActivityIndicator, Modal, StyleSheet, Text, View } from 'react-native';
import { Colors } from '../theme/colors';
import { useColorScheme } from '../hooks/use-color-scheme';

export function LoadingOverlay({ visible, title, subtitle }: { visible: boolean; title: string; subtitle?: string }) {
  const isDark = useColorScheme() === 'dark';
  const theme = isDark ? Colors.dark : Colors.light;

  if (!visible) return null;

  return (
    <Modal transparent visible={visible} animationType="fade" statusBarTranslucent>
      <View style={[styles.overlay, { backgroundColor: theme.overlayBg }]}>
        <View style={[styles.dialogCard, { backgroundColor: theme.card }]}>
          <ActivityIndicator size="large" color="#FF5722" />
          <Text style={[styles.title, { color: theme.text }]}>{title}</Text>
          {subtitle && <Text style={{ color: theme.secondaryText, fontSize: 13 }}>{subtitle}</Text>}
        </View>
      </View>
    </Modal>
  );
}
// ... StyleSheet 樣式略 ...

在頁面中(例如 clinic.tsxschedule.tsx)套用全鎖定防呆:

// 頁面中的雙重防護機制
<TextInput
  editable={!isLoading}
  style={[styles.input, { backgroundColor: isLoading ? '#E5E7EB' : theme.inputBg }]}
  value={text}
  onChangeText={setText}
/>

<TouchableOpacity disabled={isLoading} onPress={handleSubmit}>
  <Text>{isLoading ? 'AI 推演中...' : '送出'}</Text>
</TouchableOpacity>

<LoadingOverlay
  visible={isLoading}
  title="AI 教練正在重新推演課表..."
  subtitle="正在根據您的狀況進行個人化微調,請稍候"
/>

動手實作 3:個人設定中的主題 Switch 開關與版本號卡片

底層的狀態與防呆都寫好了,最後一步,我們要把這個開關實體化放到 UI 上,同時補上一個讓整個 APP 完成度瞬間拉滿的小細節:版本資訊卡片

Prompt

「我的個人設定頁面已經差不多了,現在想在畫面最下方補上兩個商業 APP 必備的細節:

  1. 一個用來切換深淺色模式的開關卡片(請用原生 Switch 元件,並根據目前狀態顯示模式來增加視覺回饋)。
  2. 最底部放一個有質感的版本資訊區塊,顯示『KAKERU v1.0.0』,並加上『Powered by Gemini』的字樣。
    請記得套用我們剛寫好的 useColorScheme Hook,讓這兩張卡片的背景與文字顏色都能自動適配主題。」
import React, { useState } from 'react';
import { ScrollView, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { useColorScheme } from '../hooks/use-color-scheme';
import { useStore } from '../store/useStore';
import { Colors } from '../theme/colors';

const APP_VERSION = '1.0.0';

export default function ProfileScreen() {
  const isDark = useColorScheme() === 'dark';
  const theme = isDark ? Colors.dark : Colors.light;
  const setThemeMode = useStore((state) => state.setThemeMode);

  const toggleTheme = () => setThemeMode(isDark ? 'light' : 'dark');

  return (
    <ScrollView style={{ backgroundColor: theme.background }}>
      {/* ... 表單欄位、BMI 卡片與儲存按鈕略 ... */}

      {/* 介面主題 Switch 開關卡片 */}
      <View style={[styles.themeCard, { backgroundColor: theme.card, borderColor: theme.cardBorder }]}>
        <View style={styles.themeToggleRow}>
          <View style={{ flex: 1 }}>
            <Text style={[styles.themeTitle, { color: theme.text }]}>
              {isDark ? ' 深色模式 (Dark Mode)' : ' 淺色模式 (Light Mode)'}
            </Text>
            <Text style={{ color: theme.secondaryText, fontSize: 12 }}>
              {isDark ? '目前為沉浸暗色風格,點擊開關切換為淺色' : '目前為明亮淺色風格,點擊開關切換為深色'}
            </Text>
          </View>
          <Switch
            value={isDark}
            onValueChange={toggleTheme}
            trackColor={{ false: '#E2E8F0', true: '#FF5722' }}
          />
        </View>
      </View>

      {/* 底部版本號碼卡片 */}
      <View style={[styles.versionCard, { backgroundColor: theme.card, borderColor: theme.cardBorder }]}>
        <Text style={[styles.versionTitle, { color: theme.text }]}>KAKERU AI 跑步教練 · v{APP_VERSION}</Text>
        <Text style={{ color: theme.mutedText, fontSize: 11 }}>
          版本號碼:{APP_VERSION} · Powered by Gemini 2.5 Flash & Vision
        </Text>
      </View>
    </ScrollView>
  );
}
// ... StyleSheet 樣式略 ...

淺色模式:
https://ithelp.ithome.com.tw/upload/images/20260904/20165043qtQW8CQ49H.png

深色模式:
https://ithelp.ithome.com.tw/upload/images/20260904/20165043DHqZEgrOHS.png

今日總結與明日預告

今天完成了上線前最後的細節優化:

  1. API 載入鎖定:在前端築起防呆機制,保障資料一致性與後端 API 額度控管;
  2. 深淺色開關:提供直覺的 Switch 開關,滿足戶外強光與夜跑護眼需求;
  3. 版本號碼資訊:確立了發布識別標準。

至此,KAKERU 的後端大腦與前端介面開發,已經在本地端全數到位。

明天,專案將正式推進至系列文的最終章:DevOps 實戰!我們將邁入「Day 27 | 後端 CI/CD:用 GitHub Actions 自動部署 Cloud Run」。

我們將大幅減少手動輸入指令,實作一個「Push-to-Deploy (推播即部署)」流程。未來只要一個簡單的 git push,GitHub Actions 的雲端機器人就會自動接手編譯、打包,並以零停機的方式無縫發佈至 GCP。我們明天見!


上一篇
Day 25 | 矛盾大對決:保留與清除跑者的血汗打卡紀錄,進階智慧合併與清除
下一篇
Day 27 | 後端 CI/CD:用 GitHub Actions 自動部署 Cloud Run
系列文
單鐵的人生如履薄冰!AI 教練 APP 30天開發旅程,你說能走到最後嗎?30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言