分类: 程序

  • 强化学习(PPO)初步接触 3 (模型微调)

    根据表格内容,进行参数的逐步优化

    代码:github

    根据上篇文档中的数据内容:

    图表解析
    imageexplained_variance 数据为负数或0附近,代表 价值网络拟合失效。
    imageapprox_kl 持续上行,策略更新幅度偏大
    image学习率 lr不变

    test_01_4

    进行参数调整:

    参数原数据变化理由
    learning_rate0.2(默认值)LinearSchedule(start=1.0, end=0.05, end_fraction=0.2)线性衰减学习率,前期快速探索,后期小幅精细更新
    clip_range0.2(默认值)0.17缩小clip,抑制approx_kl持续上涨,限制单次策略更新幅度,防止策略突变、性能跳水
    vf_coef0.5(默认值)0.8提高价值loss权重,改善explained_variance,强制网络更重视价值函数拟合
    ent_coef0.0(默认值)0.01增加基础探索,小幅熵系数持续保留探索
    gae_lambda0.95(默认值)0.96略微提升GAE,优势估计更平滑
    n_epochs10(默认值)8减少迭代轮次,减少在同一批 rollout 数据上反复更新,也是为了减少approx_kl更新上涨速度
    total_timesteps10_000100_000增加训练总步数,否则很难充分收敛

    训练后的结果

    点击查看完整日志截图

    4

    可以通过rollout/ep_rew_mean发现,价值回报的数据已经崩了。下次训练需要调整。

    test_01_5

    进行参数调整:

    参数原数据变化理由
    policy_kwargspi=[256, 256], vf=[256, 256]感觉网络不够,网络扩容,提升价值网络拟合能力
    learning_rateLinearSchedule(start=1.0, end=0.05, end_fraction=0.2)LinearSchedule(start=3e-4, end=1e-4, end_fraction=0.2)进一步压低初始梯度更新冲击,避免开局策略崩坏
    vf_coef0.80.9小幅继续提高价值损失权重,强化价值拟合
    ent_coef0.010.02提高熵系数,保留更多探索,防止提前锁死次优策略
    gae_lambda0.960.97进一步平滑GAE优势估计
    n_epochs86继续降低迭代次数,减轻单批次过度更新,缓解开局震荡
    clip_range_vf0.3新增:约束价值网络更新幅度,抑制value_loss剧烈波动
    total_timesteps10_000100_000增加训练总步数,否则很难充分收敛

    训练后的结果

    点击查看完整日志截图

    5

    可以通过rollout/ep_rew_mean发现,价值回报的数据已经开始向正常方向进行。指标都开始改善了。
    后续准备继续拉高回报。

    test_01_6

    进行参数调整:

    参数原数据变化理由
    policy_kwargspi=[256, 256], vf=[256, 256]pi=[256, 256, 128], vf=[256, 256, 128]进一步加宽网络,提升容量

    训练后的结果

    点击查看完整日志截图

    6

    回合奖励继续提升,这个是很好的现象。通过train/clip_fraction发现,后期波动很大,证明模型探索新区域时出发裁剪。

    test_01_7

    进行参数调整:

    参数原数据变化理由
    clip_range0.170.18小幅放开更新区间(train/clip_fraction后期波动大,所以修改此参数,让其波动生效)
    vf_coef0.90.95进一步强化价值拟合 (提升explained_variance的预测)
    total_timesteps100_000150_000继续增加训练总步数,让后期出现新的探索生效

    训练后的结果

    点击查看完整日志截图

    7

    训练的日志看起来都很棒了,但回报仍然没有突破0。后续训练准备继续降低探索,目标是突破回报0。

    test_01_8

    进行参数调整:

    参数原数据变化理由
    learning_rateLinearSchedule(start=3e-4, end=1e-4, end_fraction=0.2)LinearSchedule(start=3e-4, end=8e-5, end_fraction=0.2)最终学习率略微降低,让后期更新更精细
    ent_coef0.020.018轻微降低熵系数,减少不必要随机抖动,降低一点点🤏模型的“好奇心”
    total_timesteps150_000200_000继续增加训练总步数,让后期出现新的探索生效

    训练后的结果

    点击查看完整日志截图

    8

    很棒!回合回报终于大于0了!并且可以发现,explained_variance在后期已经稳定的超过了0.6。非常好的现象。
    整个模型曲线非常健康,前期没有崩盘,后期没有震荡。

    test_01_8_1_0

    基于test_01_8,继续训练50_000回合。

    训练后的结果

    点击查看完整日志截图

    8-1

    explained_variance已经可以上升到0.8了,针对理论值的1,已经非常接近了。并且ep_rew_mean已经稳定的超过了0,并且通过图表可以看出在持续上升。

    评估

    使用模型跑50次,看看模型奖励,评估一下。

    # python evaluate.py
    
    import gymnasium as gym
    from stable_baselines3 import PPO
    
    env = gym.make("LunarLander-v3")
    model = PPO.load("ppo_LunarLander")
    
    # 批量评估
    eval_episodes = 50
    total_reward = 0
    for ep in range(eval_episodes):
        obs, _ = env.reset()
        ep_r = 0
        while True:
            action, _ = model.predict(obs, deterministic=True)
            obs, reward, terminated, truncated, info = env.step(action)
            ep_r += reward
            if terminated or truncated:
                break
        total_reward += ep_r
        print(f"Episode {ep+1} reward: {ep_r:.2f}")
    print(f"平均奖励:{total_reward/eval_episodes:.2f}")
    
    env.close()
    

    结果如下:
    image

    平均奖励很高。

    最终效果对比

    未训练的帖子在:macos(M4)上跑强化学习 – PyTorch LunarLander-v3 Stable-Baselines3

    未训练训练后
    not_training-ezgif.com-video-to-gif-converter202607231650

  • 强化学习(PPO)初步接触 2 (TensorBoard log 解析)

    通用的图例数据基础介绍:

    • Value:原始真实指标数值。(图标中与图例颜色较淡的那根线)
    • Smotthed:TensorBoard 自带曲线滑动平均处理后的数值。主要用来观察整体升降趋势。(图表中和图例相同颜色的那根线)
    • Step:横轴坐标,也就是从训练开始,智能体和环境一共执行了多少步 action。
    • Relative:相对时间。用来横向对比不同实验训练速度快慢。

    Rollout

    PPO 不是走一步更新一次,而是先收集一批轨迹,再统一训练,这一批收集过程就叫一次 rollout。
    其中有2个图标:如下图
    image

    rollout/ep_len_mean

    本轮 rollout 收集到的所有回合,平均回合长度(其实就是回合执行的时间,这里用步数代替了)。如下图:
    image

    rollout/ep_rew_mean

    本轮 rollout 收集到的所有回合,平均回合奖励(就是平均回合奖励的总数)。如下图:
    image

    Time

    其中只有一个,就是fps。(这个不是游戏渲染帧率)每秒完成多少条环境交互 step,也就是训练数据采集阶段的运行速度

    简单来说,fps越高,收集训练样本速度越快,也就意味着训练速度越快。

    image

    Train

    这里有9个表格。

    train/approx_kl

    Approximate KL Divergence,近似平均KL散度,代表的是更新前旧策略 和 更新后新策略 的分布差距有多大

    PPO 最核心思想:不能让策略一次性改动太大!,否则训练极易崩溃、震荡。

    • 数值小:新旧策略几乎没变化,更新力度太弱,学习慢
    • 数值大:新策略一次性改动过猛,很危险,容易训崩
      网上有很多说法,有说0.001-0.02之间,有说0.001-0.03之间。不过没有找到详细的解析或说明。
      image

    train/clip_fraction

    被 Clip 裁剪截断的样本占全部样本的比例
    强化学习中,需要限制策略更新幅度,阻止一次性改动过大。也就意味着,更新的新策略越多,裁剪越大。所以这里有一个规律,KL 越高 → 策略改动越大 → 更多样本触发裁剪 → clip_fraction 上升
    image

    train/clip_fraction

    PPO 裁剪区间的半径,是初始化模型设置的参数。默认为0.2。可以开启动态自适应clip_range。
    image

    train/entropy_loss

    熵损失。熵(Entropy):用来衡量策略随机性大小。

    • 熵越大:动作概率分布越平均 → 智能体更喜欢随机探索
    • 熵越小:策略趋向确定性,总是固定选最优动作,探索变少
      image

    train/explained_variance

    解释方差(Explained Variance)专门用来评价 PPO 里 Critic 价值网络(估值网络)预测准不准
    数值区间解读

    • ≈ 1.0:完美!价值网络预测和真实回报几乎一致
    • > 0:价值网络有一定预测能力(有效)
    • ≈ 0:价值网络预测效果 ≈ 和直接猜平均值差不多,基本没用
    • < 0:最差情况!价值网络预测完全不靠谱,甚至还不如直接输出常数均值
      image

    train/learning_rate

    优化器当前使用的学习率 lr。其实就是控制神经网络每一次参数更新的步幅大小。

    • lr 大:参数改动幅度大,学习快,但容易震荡、训崩
    • lr 小:更新平稳,不容易翻车,但收敛速度变慢
      图中是固定学习率,没有开启线性衰减(lr_schedule)。开启后随着训练步数增加,学习率会持续慢慢降低,这条曲线会一路向下倾斜。线性衰减优势在于,开始时快速探索,后期稳定收敛,不容易崩。
      image

    train/policy_gradient_loss

    策略梯度损失(Actor 网络专属损失)
    这一步是策略更加倾向选择能拿到更高奖励的动作。只控制模型怎么选择动作。

    好的表现是:缓慢持续下降,不会剧烈震荡、不会突然断崖式飙升。
    强化学习的一个结论:平稳温和的迭代,经常比单次激进优化得到更好的最终结果。

    image

    train/value_loss

    价值网络损失(Critic 网络专属损失)
    这个目标是让 Critic 的估值越来越贴近真实收益
    价值网络:评估当前状态好不好。

    好的表现是:训练前期 Critic 完全不懂环境,预测偏差巨大;随着训练不断修正,估值误差持续缩小。平稳持续下降,没有突然剧烈跳涨。

    image

    train/loss

    图表中这个会在policy_gradient_loss和value_loss前面。只是这里我放到了最后。
    PPO 总的联合损失函数(Total Loss)
    策略梯度损失(policy_gradient_loss,优化 Actor) + 价值网络损失(优化 Critic)熵损失(entropy_loss,鼓励探索,带负号)
    强化学习中,不能像普通监督学习一样,单纯认为 loss 越低 = 模型越好!

    image

  • 强化学习(PPO)初步接触 1 (环境搭建Mac M4)

    安装python环境

    • 安装miniforge3
      正确安装后,就有了conda命令

    配置python环境

    # 创建环境,python3.11
    conda create -n rl-env python=3.11
    conda activate rl-env
    # 关闭用这个
    conda deactivate
    

    报错‘CondaError: Run ‘conda init’ before ‘conda activate’’ google 一下,init命令需要参数

    使用PyTorch进行训练,所以需要安装PyTorch

    pip3 install torch torchvision torchaudio
    

    创建文件进行测试,是否安装成功,是否是M系列芯片

    # test_env.py
    import torch
    print(torch.backends.mps.is_available()) # True = GPU可用
    print(torch.backends.mps.is_built())
    

    输出如图:
    image
    小模型完全没必要使用gpu,直接使用cpu可能速度更快。

    You are trying to run PPO on the GPU, but it is primarily intended to run on the CPU when not using a CNN policy (you are using ActorCriticPolicy which should be a MlpPolicy). See https://github.com/DLR-RM/stable-baselines3/issues/1245 for more info. You can pass device='cpu' or export CUDA_VISIBLE_DEVICES= to force using the CPU.Note: The model will train, but the GPU utilization will be poor and the training might take longer than on CPU.

    安装 gymnasium

    pip install gymnasium
    # 或
    pip install "gymnasium[box2d]"
    # 我用的这个带box2d的
    

    创建文件进行测试,是否安装成功。参考自:https://zhuanlan.zhihu.com/p/694923486

    # test_gymnasium.py
    
    import gymnasium as gym
    env = gym.make("LunarLander-v3", render_mode="human")
    observation, info = env.reset()
    
    for _ in range(100):
        action = env.action_space.sample()
        observation, reward, terminated, truncated, info = env.step(action)
        print('---')
        print("action: " + str(action))
        print("observation: " + str(observation))
        print("reward: " + str(reward))
        print("terminated: " + str(terminated))
        print("truncated: " + str(truncated))
        print("info: " + str(info))
    
        if terminated or truncated:
            observation, info = env.reset()
    
    env.close()
    

    生成如图:
    not_training-ezgif.com-video-to-gif-converter

    image

    之后将使用LunarLander-v3 这个环境进行模型训练。

    使用Stable-Baselines3进行训练

    使用Stable-Baselines3先让流程跑通

    pip install 'stable-baselines3[extra]'
    

    写个脚本开始训练进行测试

    # test_sb3.py
    import gymnasium as gym
    import torch
    from stable_baselines3 import PPO
    
    # 检测设备
    device = torch.device("mps" if torch.backends.mps.is_available() else "cpu")
    print("device: ", device)
    
    # 创建环境
    env = gym.make("LunarLander-v3")
    
    # 简单PPO测试训练
    model = PPO(
        "MlpPolicy", 
        env, 
        verbose=1, 
        device=device, 
        tensorboard_log="./ppo_logs/"  # 日志存放目录
        )
    model.learn(
        total_timesteps=10000,
        tb_log_name="test_01",    # 本次实验名称
        )
    
    # 测试运行
    obs, _ = env.reset()
    for _ in range(1000):
        action, _states = model.predict(obs)
        obs, reward, terminated, truncated, info = env.step(action)
        if terminated or truncated:
            obs, _ = env.reset()
    env.close()
    
    # 保存模型
    model.save("ppo_LunarLander")
    print("done")
    

    运行结果部分截图:
    image
    查看训练效果的方法,使用tensorboard

    tensorboard --logdir ./ppo_logs/
    

    运行后,则可以针对日志文件进行查看,打开 http://localhost:6006/
    如下图:
    image
    具体这个训练效果图要如何分析,先跳过,先尝试把流程跑完。

    使用训练的模型

    写个脚本来使用模型,看看效果:

    # test_use_ppo.py
    import gymnasium as gym
    import torch
    from stable_baselines3 import PPO
    
    # 检测设备
    device = torch.device("mps" if torch.backends.mps.is_available() else "cpu")
    print("device: ", device)
    
    model = PPO.load("ppo_LunarLander", device=device)
    
    env = gym.make("LunarLander-v3", render_mode="human")
    obs, _ = env.reset()
    
    for _ in range(1000):
        # 模型预测动作
        action, _states = model.predict(obs, deterministic=True)
        obs, reward, terminated, truncated, info = env.step(action)
        
        if terminated or truncated:
            obs, _ = env.reset()
    
    env.close()
    

    效果如下图所示:
    training-ezgif.com-video-to-gif-converter

    至此,训练就完成了。
    剩下就可以一步一步针对全流程中的内容进行拆解学习。

  • Flutter中,html 与 dart 桥接沟通

    当前有两种webview的插件,分别是:

    • webview_flutter
    • flutter_inappwebview

    webview_flutter

    dart 调用 html 方法

    dart

    其实就是dart向html发送消息,使用webViewController.runJavaScript方法,执行一段js脚本。

    // 使用 postMessage 发送
    webViewController.runJavaScript('''
    window.postMessage($message, '*');
    ''');
    

    若为map,需要先转换为json字符串,避免引号冲突。

    html

    const handleMessage = (event: MessageEvent) => {
    const data = event.data
    // 解析消息内容...
    }
    
    // 添加窗口消息监听器
    window.addEventListener('message', handleMessage)
    

    html 调用 dart 方法

    dart

    通过JavaScriptChannel,来实现监听html发送的消息的功能。
    先向页面添加 JavaScriptChannel

    //添加 JavaScript 通道用于接收 H5 消息
    webViewController.addJavaScriptChannel('FlutterChannel',
    onMessageReceived: (JavaScriptMessage message) {
    handleH5Message(message.message); // 这就是处理html发送的消息的方法
    });
    

    html

    这里的 FlutterChannel 是在dart端通过addJavaScriptChannel方法添加的,在网页端不需要声明。

    // 使用 FlutterChannel 通道发送消息(匹配 Flutter 端的 JavaScriptChannel)
    try {
    if ((window as any).FlutterChannel) {
    (window as any).FlutterChannel.postMessage(JSON.stringify(message))
    console.log('通过 FlutterChannel 发送消息到 Flutter:', message)
    } else {
    console.warn('FlutterChannel 未定义,无法发送消息到 Flutter')
    }
    } catch (error) {
    console.error('发送到 Flutter 失败:', error)
    }
    

    flutter_inappwebview

    dart 调用 html 方法

    dart

    其实就是dart向html发送消息,使用webViewController.evaluateJavascript方法,执行一段js脚本。

    // 使用 postMessage 发送
    webViewController?.evaluateJavascript(
    source: "window.top.postMessage($value, '*');",
    );
    

    若为map,需要先转换为json字符串,避免引号冲突。

    html

    const handleMessage = (event: MessageEvent) => {
    const data = event.data
    // 解析消息内容...
    }
    
    // 添加窗口消息监听器
    window.addEventListener('message', handleMessage)
    

    html 调用 dart 方法

    dart

    通过JavaScriptChannel,来实现监听html发送的消息的功能。
    先向页面添加 JavaScriptChannel

    // 添加 JavaScript Handler 用于接收 H5 消息
    webViewController!.addJavaScriptHandler(
    handlerName: 'FlutterChannel', // <--- 这个需要和 html 中的 handlerName 一致
    callback: (message) {
    if (message.isEmpty) {
    return;
    }
    handleH5Message(message[0]);
    },
    );
    

    html

    这里的 FlutterChannel 是在dart端通过addJavaScriptChannel方法添加的,在网页端不需要声明。

    // 使用 flutter_inappwebview 通道发送消息(匹配 Flutter 端的 JavaScriptChannel)
    try {
    if ((window as any).flutter_inappwebview) {
    // 调用 Dart 端的 handlerName 并发送数据
    (window as any).flutter_inappwebview.callHandler('FlutterChannel', message)
    console.log('use flutter_inappwebview send to Flutter:', message)
    } else {
    console.warn('flutter_inappwebview undefine')
    }
    } catch (error) {
    console.error('Send to Flutter failed: ', error)
    }
    

  • HarmonyOS中,html 与 ets 桥接沟通

    当前HarmonyOS中,添加Webview只有一种: Web

    流程解释

    需要先初始化webview.WebMessagePort,然后一个ets使用,发送一个端口给html去用。

    ets端

    // ...
    const HARMONY_PORT = 0;
    const HTML_PORT = 1;
    // ...
    export default struct AppWebViewPage {
    // ...
      build() {
        Web({
          src: this.url,
          controller: this.webviewController,
        })
          .onPageEnd(() => {
            try {
              this.ports = this.webviewController.createWebMessagePorts() || [];
              // 这里会返回2个端口。
              // 有点类似于 实例化了一个网页和鸿蒙之间发送消息的管道,然后把管道的两个头作为端口返回回来
              // 所以这里就有有2个端口,也就是这个通信管道的2个头
              // 那么
              // 1. 使用 HARMONY_PORT 端口,作为鸿蒙的端口,用来发送和接收消息
              // 2. 使用 HTML_PORT 端口,作为html的端口,把这个端口通过 HARMONY_PORT 端口发给html,让html来使用。
              if (!this.ports || this.ports.length < 2) {
                hilog.error(1, 'Error', '创建端口失败:返回端口数量不足');
                return;
              }
              /* 端口0, 给鸿蒙端使用 */
              this.ports[HARMONY_PORT].onMessageEvent((result: webview.WebMessage) => {
                this.onMessageEvent(result);
              });
              /* 端口1, 发送给html进行使用 */
              this.webviewController.postMessage('__init__project__', [this.ports[HTML_PORT]], '*');
            } catch (e) {
              hilog.error(1, 'Error', '初始化 WebMessagePort 失败:' + JSON.stringify(e));
            }
          })
      }
    
      /**
       * 接收到H5消息
       * @param result
       */
      private onMessageEvent(result: webview.WebMessage) {
        let msg = '';
        try {
          msg = result.toString() || '';
          hilog.info(1, 'INFO', 'onMessageEvent:' + msg);
        } catch (e) {
          hilog.error(1, 'Error', '获取消息字符串失败:' + JSON.stringify(e));
          return;
        }
      }
    
      /**
       * 向H5发布消息
       * @param message
       */
      public sendMessageToH5(message: string) {
        hilog.info(1, 'INFO', 'sendMessageToH5:' + message);
        try {
          this.ports[HARMONY_PORT].postMessageEvent(message);
        } catch (error) {
          hilog.error(1, 'Error', '向H5发布消息失败:' + JSON.stringify(error));
        }
      }
    }
    

    html 端

    <!DOCTYPE html>
    <html lang="zh-CN">
    
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Harmony H5 Demo</title>
    </head>
    
    <body>
    <h1>H5 Page</h1>
    <button onclick="sendMsgToEts()">发送消息到鸿蒙侧</button>
    <h2>日志记录</h2>
    <pre id="logHistory"></pre>
    
    <script>
        let messagePort = null;
    
        function appendLog(message) {
            const logHistory = document.getElementById("logHistory");
            const logLine = new Date().toLocaleTimeString() + " - " + message;
            logHistory.textContent = logHistory.textContent
                ? logLine + "\n" + logHistory.textContent
                : logLine;
        }
    
        appendLog("页面已初始化,等待鸿蒙侧端口...");
    
        window.addEventListener("message", function (event) {
            if (event.data === "__init__project__" && event.ports && event.ports.length > 0) {
    
                messagePort = event.ports[0];
    
                if (typeof messagePort.start === "function") {
                    messagePort.start();
                }
    
                console.log("H5 Port initialized");
                appendLog("端口初始化成功");
    
                messagePort.onmessage = function (portEvent) {
                    appendLog("收到鸿蒙侧消息: " + portEvent.data);
                };
            }
        });
    
        function sendMsgToEts() {
            if (messagePort) {
                const obj = { name: "我是html的来的消息,发给鸿蒙的", value: 123 };
                messagePort.postMessage(JSON.stringify(obj));
            } else {
                console.error("messagePort is null, Please initialize first");
                appendLog("发送失败: messagePort is null, Please initialize first");
            }
        }
    </script>
    </body>
    
    </html>
    

  • 学习自己训练一个语言模型

    学习自己训练一个语言模型

    很久之前就看到了这个项目:https://github.com/jingyaogong/minimind,直到今天才有时间来玩一玩。

    魔搭社区中,notebook有免费实例可以用。

    cpu不用试了,速度太慢了。开GPU环境玩吧,按照minimind的基础配置,预训练大约需要1个小时,蛮不错的。

  • Flutter自定义主题Theme最佳实践

    Flutter自定义主题Theme最佳实践

    Flutter中使用主题,基本都是通过ThemeData来进行的(不会有人用CupertinoThemeData吧,如果有,我敬你是个汉子)。而ThemeData的编写和定义,都牢牢的和Material Design风格绑定在了一起。
    那么,问题来了,我们在开发过程中,设计师给出的设计稿,通常带有设计师自己的一套风格主题。
    这些风格主题和被强绑定的Material Design风格,基本都是天差地别,没有一丝丝的相似。

    这就出现了一些解决方案,不过基本都是自定义一个AppColors之类的类,然后通过状态管理框架(例如:Getx、Bloc、Provider等等)进行全局刷新。
    这里就出现了一些纠结的地方:

    • Flutter中默认的Theme已经提供了切换的方法,无需再通过状态管理框架再写一次了,但是强制使用了Material Design风格,基本上和设计师给的冲突了。
    • 通过状态管理框架自己实现一套颜色配置,完全抛弃Theme提供的所有帮助。

    其实,我们可以通过ThemeExtension来进行扩展,把我们的theme进行扩展。
    来,上教程!

    1. 先来一个AppColors压压惊

    当然,无论怎样,都需要定义我们的AppColors。在这里,用来存放所有的设计师给的规范颜色值。
    例如:

    import 'package:flutter/material.dart';
    
    /// app_colors.dart
    
    class AppLightColors {
      static const Color demo1 = Colors.red; // 红色的
      static const Color demo2 = Colors.green; // 绿色的
      // ... 这里接着写设计师提供的所有颜色值
    }
    
    class AppDarkColors {
      static const Color demo1 = Colors.black; // 黑色的
      static const Color demo2 = Colors.white; // 白色的
      // ... 这里接着写设计师提供的所有颜色值
    }
    

    2. 定义我们自己的Theme

    接着,我们就要创建我们自己的Theme了,通过ThemeExtension来实现,这里有几点需要注意:

    • 如果颜色值很多(基本都会很多),尽量使用ai来帮你实现复制粘贴内容,这里的颜色值需要完完整整的都定义出来,跟着设计师给出的颜色表。
    • lerp方法是一个渐变方法,切换主题后,所有颜色会有一个渐变效果,很好看。可以直接使用Color.lerp方法进行。
      例如:
    import 'package:flutter/material.dart';
    
    /// app_colors_extension.dart
    
    class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
      AppColorsExtension({
        required this.demo1,
        required this.demo2,
        // ... 这里需要定义所有的值,如果写的累就找AI吧
      });
    
      final Color demo1; // 演示值1
      final Color demo2; // 演示值2
      // ... 这里需要定义所有的值,如果写的累就找AI吧
    
      @override
      ThemeExtension<AppColorsExtension> copyWith({Color? demo1, Color? demo2}) {
        return AppColorsExtension(
          demo1: demo1 ?? this.demo1,
          demo2: demo2 ?? this.demo2,
          // ... 这里需要定义所有的值,如果写的累就找AI吧
        );
      }
    
      @override
      ThemeExtension<AppColorsExtension> lerp(
        ThemeExtension<AppColorsExtension>? other,
        double t,
      ) {
        // 这里排除了不是我们自定义的情况
        if (other is! AppColorsExtension) {
          return this;
        }
    
        return AppColorsExtension(
          demo1: Color.lerp(demo1, other.demo1, t)!,
          demo2: Color.lerp(demo2, other.demo2, t)!,
          // ... 这里需要定义所有的值,如果写的累就找AI吧
        );
      }
    }
    

    3. 写我们的Theme吧

    前两步,我们定义好了ThemeExtension和Color,现在,通过自定义的theme,我们把ThemeExtension和Color绑定在一起。
    例如:

    import 'package:flutter/material.dart';
    
    import 'app_colors.dart';
    import 'app_colors_extension.dart';
    
    /// app_themes.dart
    
    // 我们自定义的亮色主题
    class LightTheme {
      static ThemeData theme = ThemeData.light().copyWith(
        extensions: [_lightAppColors],
      );
    
      static final _lightAppColors = AppColorsExtension(
        demo1: AppLightColors.demo1,
        demo2: AppLightColors.demo2,
      );
    }
    
    // 我们自定义的暗色主题
    class DarkTheme {
      static ThemeData theme = ThemeData.dark().copyWith(
        extensions: [_darkAppColors],
      );
    
      static final _darkAppColors = AppColorsExtension(
        demo1: AppDarkColors.demo1,
        demo2: AppDarkColors.demo2,
      );
    }
    

    4. 把自定的Theme配置到MaterialApp中

    上一步中我们写好了亮色主题和暗色主题,那么我们把这个主题绑定到Flutter的项目中。
    例如:

    import 'package:flutter/material.dart';
    
    import 'home_page.dart';
    import 'app_themes.dart';
    
    /// main.dart
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatefulWidget {
      const MyApp({super.key});
    
      @override
      State<MyApp> createState() => _MyAppState();
    }
    
    class _MyAppState extends State<MyApp> {
      ThemeMode _themeMode = ThemeMode.light; // 默认设置为light
    
      // 这里定义一个切换主题的方法,提供给HomePage使用,这里看大家的状态管理框架要怎么处理了
      void toggleTheme() {
        setState(() {
          _themeMode = _themeMode == ThemeMode.light
              ? ThemeMode.dark
              : ThemeMode.light;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Theme Demo',
          theme: AppLightTheme.theme, // 这里定义亮色主题
          darkTheme: AppDarkTheme.theme, // 这里定义暗色主题
          themeMode: _themeMode, // 设置主题模式,这里就要看大家的状态管理框架要怎么处理了
          home: HomePage(toggleTheme: toggleTheme),
        );
      }
    }
    
    

    5. 用起来用起来

    至此,我们所有的配置工作就全部做完了,现在,就开始用起来。
    获取颜色的方法:Theme.of(context).extension<AppColorsExtension>()?.demo1
    例如:

    import 'package:flutter/material.dart';
    
    import 'app_colors_extension.dart';
    
    /// home_page.dart
    
    class HomePage extends StatelessWidget {
      const HomePage({super.key, required this.toggleTheme});
    
      final void Function() toggleTheme;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('Flutter Theme Demo')),
          body: Center(
            child: Column(
              mainAxisAlignment: .center,
              children: [
                Container(
                  decoration: BoxDecoration(
                    color: Theme.of(
                      context,
                    ).extension<AppColorsExtension>()!.demo1, // 这里就是使用我们自定义的颜色
                    border: Border.all(width: 1, color: Colors.grey),
                  ),
                  height: 120,
                  width: 120,
                ),
                Container(
                  decoration: BoxDecoration(
                    color: Theme.of(
                      context,
                    ).extension<AppColorsExtension>()?.demo2, // 这里就是使用我们自定义的颜色
                    border: Border.all(width: 1, color: Colors.grey),
                  ),
                  height: 120,
                  width: 120,
                ),
                ElevatedButton(
                  onPressed: () {
                    // 点击这里切换主题, 从light切换到dark, 从dark切换到light
                    toggleTheme();
                  },
                  child: Text("切换主题"),
                ),
              ],
            ),
          ),
        );
      }
    }
    

    这样,我们的代码就都写好啦。

    6. 来看看效果

    这就是效果咯,来看一下,感受一下

    浅色深色动画效果

    7. 一点点简化的小方法

    我们看到,第5步中,获取颜色的方法Theme.of(context).extension<AppColorsExtension>()?.demo1,写起来有点长,我们来化简一下:
    例如:
    首先,得修改一下app_themes.dart文件,来提供默认的颜色

    // ......
    // 我们自定义的亮色主题
    class AppLightTheme {
      static ThemeData theme = ThemeData.light().copyWith(
        extensions: [lightAppColors], // <-- 改了这里,把下划线去掉,变为public字段
      );
    
      // 改了这里,把下划线去掉,变为public字段
      static final lightAppColors = AppColorsExtension(
        demo1: AppLightColors.demo1,
        demo2: AppLightColors.demo2,
      );
    }
    // ......
    

    然后,我们新增一个Extension类,来简化颜色的调用:

    import 'package:flutter/material.dart';
    
    import 'app_colors_extension.dart';
    import 'app_themes.dart';
    
    /// theme_extension.dart
    
    extension AppThemeExtension on ThemeData {
      /// 用法: Theme.of(context).appColors;
      AppColorsExtension get appColors =>
          extension<AppColorsExtension>() ?? AppLightTheme.lightAppColors;
    }
    

    这时,我们调用的时候,可以使用Theme.of(context).appColors.demo1来进行。
    例如:

    import 'package:flutter/material.dart';
    
    import 'theme_extension.dart'; // 这里就需要引入这个类来使用我们的简化方法
    // import 'app_colors_extension.dart';   <-- 这个就不用啦
    
    /// home_page.dart
    
    class HomePage extends StatelessWidget {
      const HomePage({super.key, required this.toggleTheme});
    
      final void Function() toggleTheme;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('Flutter Theme Demo')),
          body: Center(
            child: Column(
              mainAxisAlignment: .center,
              children: [
                Container(
                  decoration: BoxDecoration(
                    color: Theme.of(context).appColors.demo1, // 这里就是使用我们自定义的颜色
                    border: Border.all(width: 1, color: Colors.grey),
                  ),
                  height: 120,
                  width: 120,
                ),
                Container(
                  decoration: BoxDecoration(
                    color: Theme.of(context).appColors.demo2, // 这里就是使用我们自定义的颜色
                    border: Border.all(width: 1, color: Colors.grey),
                  ),
                  height: 120,
                  width: 120,
                ),
                ElevatedButton(
                  onPressed: () {
                    // 点击这里切换主题, 从light切换到dark, 从dark切换到light
                    toggleTheme();
                  },
                  child: Text("切换主题"),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    

    8. 结束

    至此,主题配置就完成了。祝大家玩得开心。

  • 还是习惯了用Qoder

    还是习惯了用Qoder

    Qoder 贵是贵了点,但的确是好用呀。

    来一个我的推广链接:点我去购买