Vue 开发者快速上手 Flutter(三)

第 3 部分 · 熟悉基础 Widget(Vue ↔ Flutter)

前两篇我们把"心智模型"和一个 Todo 练习啃下来了,这一篇专攻基础 Widget。每个块我都会给一个 Vue ↔ Flutter 对照 demo,可以直接拷走跑。


一、3~4 天的学习节奏

我自己摸索时走过最大的弯路是"一次性把所有 Widget 都过一遍",结果什么都记不住。后来改成按"功能块"切分,每天只啃一类,效果好太多。建议你也照这个节奏走:

主题 核心 Widget 对应 Vue/HTML 概念
Day 1 布局 Container Row Column Padding Center SizedBox Expanded Stack div + flex + 定位
Day 2 基础显示 Text Image Icon Card Divider <p> <img> <i>
Day 3 列表与滚动 ListView ListView.builder GridView SingleChildScrollView RefreshIndicator v-for + scroll
Day 4 交互与表单 TextField ElevatedButton Checkbox Switch GestureDetector Scaffold AppBar <input> <button> 事件

二、Day 1:布局三件套

布局是 Flutter 上手最劝退的部分------倒不是它难,而是 Vue/CSS 老司机会一直想"我应该写个 div 然后......",但 Flutter 没有 div,一切都是 Widget

幸运的是,会下面这三个,80% 的页面都能搭出来。

1. Row / Column ------ 等同于 flex

dart 复制代码
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween, // justify-content
  crossAxisAlignment: CrossAxisAlignment.center,     // align-items
  children: [Text('左'), Text('右')],
)

对照 CSS: display: flex; justify-content: space-between; align-items: center;

记住这个映射就够了:

Flex 属性 Flutter 属性(在 Row/Column 上)
flex-direction: row Row(...)
flex-direction: column Column(...)
justify-content mainAxisAlignment
align-items crossAxisAlignment

小坑:Row 的"主轴"是水平的,Column 的"主轴"是垂直的。所以 mainAxisAlignment 在 Row 里管左右、在 Column 里管上下。别用 justify / align 的肌肉记忆去死记。

2. Expanded / Flexible ------ 等同于 flex: 1

dart 复制代码
Row(children: [
  Text('固定'),
  Expanded(child: Text('占满剩余')),  // ≈ flex: 1
])

ExpandedFlexible 更"霸道",会强制吃满剩余空间;Flexible 则可以小于内容尺寸。日常 90% 用 Expanded 就够。

3. Container ------ 万能盒子(≈ div + style)

dart 复制代码
Container(
  width: 100,
  height: 100,
  margin: EdgeInsets.all(8),
  padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(8),
    boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 4)],
  ),
  child: Text('Hello'),
)

注意一个细节:decoration 的时候 color 要写在 decoration 里 ,不能直接写在 Container 上,否则会报错。这是新手最常踩的坑之一。

4. Stack ------ 等同于 position: absolute

dart 复制代码
Stack(
  children: [
    Image.network('https://picsum.photos/300/120'),
    Positioned(
      top: 8, right: 8,
      child: Container(
        padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
        color: Colors.red,
        child: Text('NEW', style: TextStyle(color: Colors.white)),
      ),
    ),
  ],
)

Stack + Positioned 就是 CSS 里的 position: relative + absolute

完整对照 demo

Vue 版本(Layout.vue):

vue 复制代码
<template>
  <div class="container">
    <div class="row space-between center">
      <div class="box red">A</div>
      <div class="box green">B</div>
      <div class="box blue">C</div>
    </div>

    <div class="column" style="height: 120px;">
      <div class="box yellow">头</div>
      <div class="box flex-1">中间占满</div>
      <div class="box yellow">尾</div>
    </div>

    <div class="card">这是一个圆角阴影卡片</div>

    <div class="stack">
      <img src="https://picsum.photos/300/120" />
      <span class="badge">NEW</span>
    </div>
  </div>
</template>

<style scoped>
.row { display: flex; flex-direction: row; }
.column { display: flex; flex-direction: column; }
.space-between { justify-content: space-between; }
.center { align-items: center; }
.flex-1 { flex: 1; }
.box { padding: 12px; margin: 4px; color: white; }
.red { background: crimson; }
.green { background: seagreen; }
.blue { background: steelblue; }
.yellow { background: goldenrod; }
.card {
  margin: 12px 0; padding: 16px;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.stack { position: relative; display: inline-block; }
.badge {
  position: absolute; top: 8px; right: 8px;
  background: red; color: white;
  padding: 2px 6px; border-radius: 4px; font-size: 12px;
}
</style>

Flutter 版本(layout_demo.dart):

dart 复制代码
import 'package:flutter/material.dart';

class LayoutDemo extends StatelessWidget {
  const LayoutDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Layout Demo')),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Row + 主轴/交叉轴对齐(≈ flex + justify-content + align-items)
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: const [
                _Box(label: 'A', color: Colors.red),
                _Box(label: 'B', color: Colors.green),
                _Box(label: 'C', color: Colors.blue),
              ],
            ),

            const SizedBox(height: 16),

            // Column + Expanded(≈ flex-direction:column + flex:1)
            SizedBox(
              height: 120,
              child: Column(
                children: const [
                  _Box(label: '头', color: Colors.orange),
                  Expanded(child: _Box(label: '中间占满', color: Colors.purple)),
                  _Box(label: '尾', color: Colors.orange),
                ],
              ),
            ),

            const SizedBox(height: 16),

            // Container 样式(圆角、阴影、内外边距)
            Container(
              margin: const EdgeInsets.symmetric(vertical: 8),
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(8),
                boxShadow: const [
                  BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2)),
                ],
              ),
              child: const Text('这是一个圆角阴影卡片'),
            ),

            const SizedBox(height: 16),

            // Stack(绝对定位)≈ CSS position: absolute
            Stack(
              children: [
                Image.network('https://picsum.photos/300/120'),
                Positioned(
                  top: 8,
                  right: 8,
                  child: Container(
                    padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
                    color: Colors.red,
                    child: const Text('NEW',
                        style: TextStyle(color: Colors.white, fontSize: 12)),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class _Box extends StatelessWidget {
  final String label;
  final Color color;
  const _Box({required this.label, required this.color});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(12),
      margin: const EdgeInsets.all(4),
      color: color,
      child: Text(label, style: const TextStyle(color: Colors.white)),
    );
  }
}

跑一遍对比效果,你会发现:在 Flutter 里,"样式"和"结构"是同一棵树,没有 CSS 文件、没有 class 选择器、没有作用域问题。这是和 Vue 最大的心智差异。


三、Day 2:基础显示(文本/图片/图标/卡片)

1. Text ------ 比 <p> 强大很多

dart 复制代码
Text(
  '你好,Flutter',
  style: TextStyle(
    fontSize: 16,
    color: Colors.red,
    fontWeight: FontWeight.bold,
    height: 1.5,           // line-height
    letterSpacing: 0.5,
  ),
  maxLines: 2,
  overflow: TextOverflow.ellipsis, // 省略号
)

注意 maxLines + overflow 的组合,这是 Flutter 处理长文本的标配,比 CSS 的 -webkit-line-clamp 简单多了。

要在一段文字里混排不同样式?用 RichText

dart 复制代码
RichText(
  text: TextSpan(
    style: TextStyle(color: Colors.black, fontSize: 14),
    children: [
      TextSpan(text: '价格:'),
      TextSpan(text: '¥99', style: TextStyle(color: Colors.red, fontSize: 18)),
      TextSpan(text: ' 起'),
    ],
  ),
)

2. Image ------ 网络/资源/文件三种姿势

dart 复制代码
// 网络图
Image.network('https://picsum.photos/200')

// 资源图(pubspec.yaml 里要先声明 assets)
Image.asset('assets/logo.png')

// 文件图
Image.file(File('/path/to/img.jpg'))

fit 参数等同于 CSS 的 object-fit

dart 复制代码
Image.network(
  url,
  width: 100, height: 100,
  fit: BoxFit.cover, // contain / cover / fill / fitWidth / fitHeight
)

3. Icon ------ Material 图标库直接用

dart 复制代码
Icon(Icons.favorite, color: Colors.red, size: 24)

不用引第三方图标库、不用配字体文件,Icons.xxx 几千个图标开箱即用。

4. Card + Divider ------ 卡片和分割线

dart 复制代码
Card(
  elevation: 2,
  margin: EdgeInsets.all(8),
  child: Padding(
    padding: EdgeInsets.all(12),
    child: Column(
      children: [
        Text('标题', style: TextStyle(fontWeight: FontWeight.bold)),
        Divider(),
        Text('内容内容内容'),
      ],
    ),
  ),
)

Card 自带圆角和阴影,比自己用 Container + BoxDecoration 写要省心。


四、Day 3:列表与滚动

这一块是 Flutter 体验明显优于 Vue 的地方------长列表的虚拟滚动是官方内置的。

短列表 vs 长列表的黄金法则

场景 Vue Flutter
短列表 (<20 项) v-for Column(children: items.map(...).toList())ListView(children: [...])
长列表(按需渲染) 需手动分页 / vue-virtual-scroller ListView.builder(自带懒加载)
网格 CSS grid GridView.builder
下拉刷新 自己写 RefreshIndicator

黄金法则:只要列表可能 > 20 项,立刻用 ListView.builder,不要用 Column + ...map

Column + map 会把所有 child 一次性 build 出来,列表一长就掉帧;ListView.builder 只 build 屏幕上能看到的几个,1000 项也丝滑。

三种列表写法

dart 复制代码
// 1. 短列表(直接列出)
ListView(
  children: const [
    ListTile(title: Text('苹果')),
    ListTile(title: Text('香蕉')),
    ListTile(title: Text('橙子')),
  ],
)

// 2. 长列表(懒加载)
ListView.builder(
  itemCount: 1000,
  itemBuilder: (ctx, i) => ListTile(title: Text('第 ${i + 1} 项')),
)

// 3. 带分割线的长列表
ListView.separated(
  itemCount: 1000,
  itemBuilder: (ctx, i) => ListTile(title: Text('第 $i 项')),
  separatorBuilder: (ctx, i) => const Divider(height: 1),
)

下拉刷新(一行代码搞定)

dart 复制代码
RefreshIndicator(
  onRefresh: () async {
    await fetchData();
  },
  child: ListView.builder(...),
)

Vue 这边要么自己写 touch 事件,要么引第三方库;Flutter 直接套一层 RefreshIndicator 就完事了。

完整对照 demo

Vue 版本(ListDemo.vue):

vue 复制代码
<template>
  <div>
    <h3>普通列表(短)</h3>
    <ul>
      <li v-for="item in shortList" :key="item">{{ item }}</li>
    </ul>

    <h3>长列表(虚拟滚动需自己写)</h3>
    <ul style="max-height: 200px; overflow-y: auto;">
      <li v-for="i in longList" :key="i">第 {{ i }} 项</li>
    </ul>

    <h3>网格</h3>
    <div class="grid">
      <div v-for="i in 9" :key="i" class="cell">{{ i }}</div>
    </div>

    <h3>下拉刷新(伪代码)</h3>
    <button @click="refresh">手动刷新</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const shortList = ['苹果', '香蕉', '橙子']
const longList = ref(Array.from({ length: 1000 }, (_, i) => i + 1))

async function refresh() {
  await new Promise(r => setTimeout(r, 800))
  longList.value = Array.from({ length: 1000 }, (_, i) => i + 1)
}
</script>

<style scoped>
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.cell { background: #eee; padding: 16px; text-align: center; }
</style>

Flutter 版本(list_demo.dart):

dart 复制代码
import 'package:flutter/material.dart';

class ListDemo extends StatefulWidget {
  const ListDemo({super.key});

  @override
  State<ListDemo> createState() => _ListDemoState();
}

class _ListDemoState extends State<ListDemo> {
  List<int> longList = List.generate(1000, (i) => i + 1);

  Future<void> _refresh() async {
    await Future.delayed(const Duration(milliseconds: 800));
    setState(() {
      longList = List.generate(1000, (i) => i + 1);
    });
  }

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('List Demo'),
          bottom: const TabBar(tabs: [
            Tab(text: '短列表'),
            Tab(text: '长列表'),
            Tab(text: '网格'),
          ]),
        ),
        body: TabBarView(
          children: [
            // 1. 短列表:直接 ListView + children
            ListView(
              children: const [
                ListTile(title: Text('苹果')),
                ListTile(title: Text('香蕉')),
                ListTile(title: Text('橙子')),
              ],
            ),

            // 2. 长列表:builder(懒加载)+ 下拉刷新
            RefreshIndicator(
              onRefresh: _refresh,
              child: ListView.builder(
                itemCount: longList.length,
                itemBuilder: (ctx, i) => ListTile(title: Text('第 ${longList[i]} 项')),
              ),
            ),

            // 3. 网格
            GridView.builder(
              padding: const EdgeInsets.all(8),
              itemCount: 9,
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3,
                mainAxisSpacing: 8,
                crossAxisSpacing: 8,
              ),
              itemBuilder: (ctx, i) => Container(
                color: Colors.grey.shade300,
                alignment: Alignment.center,
                child: Text('${i + 1}'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

五、Day 4:交互与表单

1. TextField ------ 等同于 <input>

dart 复制代码
final ctrl = TextEditingController();

TextField(
  controller: ctrl,
  decoration: InputDecoration(
    labelText: '账号',
    hintText: '请输入账号',
    prefixIcon: Icon(Icons.person),
    border: OutlineInputBorder(),
  ),
  onChanged: (v) => print(v),
  onSubmitted: (v) => print('回车: $v'),
)

// 别忘了在 dispose 中:ctrl.dispose();

Vue 用 v-model 一行就双向绑定,Flutter 这边要么用 controller 取值,要么 onChanged 自己存 state。习惯了就好------这是为了让数据流单向、可追踪。

2. 按钮们

dart 复制代码
ElevatedButton(onPressed: () {}, child: Text('提交')),
TextButton(onPressed: () {}, child: Text('文字按钮')),
OutlinedButton(onPressed: () {}, child: Text('描边按钮')),
IconButton(icon: Icon(Icons.search), onPressed: () {}),

关键点:onPressed: null 时按钮自动变灰禁用 。所以要禁用按钮,传 null 就行,不用单独写 disabled 属性:

dart 复制代码
ElevatedButton(
  onPressed: canSubmit ? _submit : null, // null = disabled
  child: Text('登录'),
)

3. Checkbox / Switch

dart 复制代码
CheckboxListTile(
  value: _remember,
  onChanged: (v) => setState(() => _remember = v ?? false),
  title: const Text('记住我'),
)

SwitchListTile(
  value: _darkMode,
  onChanged: (v) => setState(() => _darkMode = v),
  title: const Text('深色模式'),
)

PC 上习惯用 Checkbox,但移动端 Switch 更常见,根据场景挑。

4. GestureDetector / InkWell ------ 给任意 Widget 加点击

dart 复制代码
// 想要点击水波纹效果(Material 风格)→ InkWell
InkWell(
  onTap: () {},
  child: Container(padding: EdgeInsets.all(8), child: Text('点我')),
)

// 想要纯手势检测,无视觉反馈 → GestureDetector
GestureDetector(
  onTap: () {},
  onDoubleTap: () {},
  onLongPress: () {},
  child: ...,
)

5. Scaffold ------ 整页脚手架

这是 Flutter 给的"福利"------一整页的标准布局(顶栏 + 内容 + 底栏 + 浮动按钮 + 抽屉)官方一个 Widget 就解决了。Vue 这边没有内置脚手架,要么自己写,要么引 Element Plus 这种 UI 库。

dart 复制代码
Scaffold(
  appBar: AppBar(title: Text('页面标题')),
  body: ...,
  floatingActionButton: FloatingActionButton(
    onPressed: ...,
    child: Icon(Icons.add),
  ),
  bottomNavigationBar: BottomNavigationBar(...),
  drawer: Drawer(...), // 侧边栏
)

完整对照 demo(登录表单)

Vue 版本(FormDemo.vue):

vue 复制代码
<template>
  <form @submit.prevent="submit" class="form">
    <label>账号</label>
    <input v-model="username" placeholder="请输入账号" />

    <label>密码</label>
    <input v-model="password" type="password" placeholder="请输入密码" />

    <label><input type="checkbox" v-model="remember" /> 记住我</label>
    <label><input type="checkbox" v-model="darkMode" /> 深色模式</label>

    <button type="submit" :disabled="!canSubmit">登录</button>

    <p v-if="msg" :style="{ color: 'red' }">{{ msg }}</p>
  </form>
</template>

<script setup>
import { ref, computed } from 'vue'

const username = ref('')
const password = ref('')
const remember = ref(false)
const darkMode = ref(false)
const msg = ref('')

const canSubmit = computed(() => username.value && password.value)

function submit() {
  if (!canSubmit.value) {
    msg.value = '请填写完整'
    return
  }
  msg.value = ''
  alert(`登录:${username.value} / 记住=${remember.value}`)
}
</script>

<style scoped>
.form { display: flex; flex-direction: column; gap: 8px; max-width: 320px; }
input { padding: 8px; }
</style>

Flutter 版本(form_demo.dart):

dart 复制代码
import 'package:flutter/material.dart';

class FormDemo extends StatefulWidget {
  const FormDemo({super.key});

  @override
  State<FormDemo> createState() => _FormDemoState();
}

class _FormDemoState extends State<FormDemo> {
  final _usernameCtrl = TextEditingController();
  final _passwordCtrl = TextEditingController();
  bool _remember = false;
  bool _darkMode = false;
  String _msg = '';

  bool get _canSubmit =>
      _usernameCtrl.text.isNotEmpty && _passwordCtrl.text.isNotEmpty;

  @override
  void dispose() {
    _usernameCtrl.dispose();
    _passwordCtrl.dispose();
    super.dispose();
  }

  void _submit() {
    if (!_canSubmit) {
      setState(() => _msg = '请填写完整');
      return;
    }
    setState(() => _msg = '');
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('登录:${_usernameCtrl.text} / 记住=$_remember')),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Form Demo')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            TextField(
              controller: _usernameCtrl,
              decoration: const InputDecoration(
                labelText: '账号',
                hintText: '请输入账号',
                prefixIcon: Icon(Icons.person),
                border: OutlineInputBorder(),
              ),
              onChanged: (_) => setState(() {}),
            ),
            const SizedBox(height: 12),

            TextField(
              controller: _passwordCtrl,
              obscureText: true,
              decoration: const InputDecoration(
                labelText: '密码',
                hintText: '请输入密码',
                prefixIcon: Icon(Icons.lock),
                border: OutlineInputBorder(),
              ),
              onChanged: (_) => setState(() {}),
            ),
            const SizedBox(height: 8),

            CheckboxListTile(
              value: _remember,
              onChanged: (v) => setState(() => _remember = v ?? false),
              title: const Text('记住我'),
              contentPadding: EdgeInsets.zero,
            ),

            SwitchListTile(
              value: _darkMode,
              onChanged: (v) => setState(() => _darkMode = v),
              title: const Text('深色模式'),
              contentPadding: EdgeInsets.zero,
            ),

            const SizedBox(height: 12),
            ElevatedButton(
              onPressed: _canSubmit ? _submit : null,
              child: const Text('登录'),
            ),

            if (_msg.isNotEmpty)
              Padding(
                padding: const EdgeInsets.only(top: 8),
                child: Text(_msg, style: const TextStyle(color: Colors.red)),
              ),
          ],
        ),
      ),
    );
  }
}

完整对照 demo(整页脚手架)

Vue 版本(PageScaffold.vue):

vue 复制代码
<template>
  <!-- Vue 里没有内置脚手架,通常自己写或借助 element-plus 等 UI 库 -->
  <div class="page">
    <header class="app-bar">
      <button @click="back">←</button>
      <h2>页面标题</h2>
      <button @click="more">⋮</button>
    </header>

    <main class="body">
      <p>这里是页面正文</p>
    </main>

    <button class="fab" @click="onFab">+</button>

    <nav class="bottom-bar">
      <button :class="{active: tab===0}" @click="tab=0">首页</button>
      <button :class="{active: tab===1}" @click="tab=1">消息</button>
      <button :class="{active: tab===2}" @click="tab=2">我的</button>
    </nav>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const tab = ref(0)
function back() {}
function more() {}
function onFab() { alert('FAB!') }
</script>

<style scoped>
.page { display: flex; flex-direction: column; height: 100vh; }
.app-bar { display: flex; justify-content: space-between; align-items: center;
           padding: 8px 12px; background: #4caf50; color: white; }
.body { flex: 1; padding: 16px; }
.fab { position: fixed; right: 16px; bottom: 80px;
       width: 56px; height: 56px; border-radius: 50%;
       background: #ff5722; color: white; border: none; font-size: 24px; }
.bottom-bar { display: flex; }
.bottom-bar button { flex: 1; padding: 12px; }
.bottom-bar button.active { color: #4caf50; }
</style>

Flutter 版本(scaffold_demo.dart):

dart 复制代码
import 'package:flutter/material.dart';

class ScaffoldDemo extends StatefulWidget {
  const ScaffoldDemo({super.key});

  @override
  State<ScaffoldDemo> createState() => _ScaffoldDemoState();
}

class _ScaffoldDemoState extends State<ScaffoldDemo> {
  int _tab = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('页面标题'),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () => Navigator.maybePop(context),
        ),
        actions: [
          IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}),
        ],
      ),
      body: const Center(child: Text('这里是页面正文')),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('FAB!')),
          );
        },
        child: const Icon(Icons.add),
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _tab,
        onTap: (i) => setState(() => _tab = i),
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
          BottomNavigationBarItem(icon: Icon(Icons.message), label: '消息'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
        ],
      ),
    );
  }
}

把 Vue 版的 60 多行 CSS + 模板和 Flutter 版的 40 行声明式代码放一起,差距就出来了:移动端那些通用模式,Flutter 直接给你打包好了

六、问题总结

最后总结一下我自己上手时反复栽跟头的几个点,写在这里给后来人省点血:

  1. Containercolordecoration.color 不能并存 ------ 有 decoration 时 color 必须写在 decoration 里。
  2. 想横排不一定要 Row ------ 文字混排用 Text.rich / RichText 更合适,Row 会让每个 child 各占一行高度。
  3. Expanded 必须在 Row/Column/Flex 里 ------ 单独用会报"Incorrect use of ParentDataWidget"。
  4. Image.network 要给宽高 ------ 不然布局期间不知道占多大,可能闪一下。
  5. TextEditingController 记得 dispose ------ Vue 里 ref 自动回收,Flutter 这边手动管,忘了就是内存泄漏。
相关推荐
JavaAgent架构师1 小时前
前端AI工程化(六):Function Calling与RAG前端实践
前端·人工智能
用户11481867894841 小时前
Vue 开发者快速上手 Flutter(一)
前端
鹏多多2 小时前
Trae cn里使用Pencil来制作设计图的手把手教程
前端·ai编程·trae
客场消音器2 小时前
如何使用codex进行UI重构,让AI开发的前端页面不再千篇一律
前端·后端·微信小程序
大家的林语冰2 小时前
Canvas 文艺复兴,HTML-in-Canvas 炫酷特效摆拍走红,Canvas 中也能渲染交互式的 HTML 元素了
前端·javascript·html
WebGirl2 小时前
Visual Studio Code (VSCode) 中配置 MCP
前端
JarvanMo3 小时前
Fluwx 6.0 预览版本他来了
前端
KaMeidebaby3 小时前
卡梅德生物技术快报|单 B 细胞抗体筛选服务:技术架构、流程实现与数据验证
前端·数据库·其他·百度·新浪微博