跳转至

03-Widget 基础

写个简单界面,结合 Java Swing 对比学习 Flutter Widget


1. 什么是 Widget?

Flutter 的一切皆 Widget。

Widget = 界面组件 = Java Swing 的 JButton / JLabel / JPanel
Flutter Java Swing 说明
Text JLabel 显示文本
IconButton JButton 按钮
Container JPanel 容器,可放其他组件
Column BoxLayout Y 垂直排列
Row BoxLayout X 水平排列
Scaffold JFrame 页面骨架

2. 最简单的 Widget

import 'package:flutter/material.dart';

void main() {
    runApp(
        // MaterialApp = App 根容器,类似 JFrame
        MaterialApp(
            home: Scaffold(
                // Scaffold = 页面骨架,提供 AppBar/Body/BottomNav
                appBar: AppBar(title: Text('我的App')),
                body: Center(child: Text('Hello Vexfy!')),
            ),
        ),
    );
}

对应 Java Swing:

public class MyApp {
    public static void main(String[] args) {
        JFrame frame = new JFrame("我的App");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new JLabel("Hello Vexfy!", SwingConstants.CENTER));
        frame.pack();
        frame.setVisible(true);
    }
}

3. Vexfy 的真实 Widget 代码

// 文件:app/lib/app/modules/player/player_page.dart

class PlayerPage extends GetView<PlayerController> {
    const PlayerPage({super.key});

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: const Text('播放器'),
                backgroundColor: Colors.black,
            ),

            body: Container(
                width: double.infinity,
                height: double.infinity,
                color: Colors.grey[900],

                child: Column(
                    children: [
                        // 封面
                        Container(
                            width: 300,
                            height: 300,
                            margin: const EdgeInsets.only(top: 50),
                            decoration: BoxDecoration(
                                borderRadius: BorderRadius.circular(20),
                            ),
                            child: ClipRRect(
                                borderRadius: BorderRadius.circular(20),
                                child: Image.network(
                                    controller.currentSong.value.coverUrl,
                                    fit: BoxFit.cover,
                                ),
                            ),
                        ),

                        // 歌名 + 歌手
                        Padding(
                            padding: const EdgeInsets.all(20),
                            child: Column(
                                children: [
                                    Text(
                                        controller.currentSong.value.title,
                                        style: const TextStyle(
                                            fontSize: 24,
                                            color: Colors.white,
                                            fontWeight: FontWeight.bold,
                                        ),
                                    ),
                                    const SizedBox(height: 8),
                                    Text(
                                        controller.currentSong.value.artist,
                                        style: TextStyle(
                                            fontSize: 16,
                                            color: Colors.grey[400],
                                        ),
                                    ),
                                ],
                            ),
                        ),

                        // 播放控制按钮
                        Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                                IconButton(
                                    icon: const Icon(Icons.skip_previous),
                                    iconSize: 50,
                                    color: Colors.white,
                                    onPressed: controller.previous,
                                ),
                                IconButton(
                                    icon: Icon(
                                        controller.isPlaying.value
                                            ? Icons.pause_circle_filled
                                            : Icons.play_circle_filled,
                                    ),
                                    iconSize: 70,
                                    color: Colors.white,
                                    onPressed: controller.playPause,
                                ),
                                IconButton(
                                    icon: const Icon(Icons.skip_next),
                                    iconSize: 50,
                                    color: Colors.white,
                                    onPressed: controller.next,
                                ),
                            ],
                        ),
                    ],
                ),
            ),
        );
    }
}

4. 常用 Widget 速查

4.1 文本 Widget

Text(
    '歌名',
    style: TextStyle(
        fontSize: 24,
        color: Colors.white,
        fontWeight: FontWeight.bold,
    ),
)

4.2 按钮 Widget

IconButton(
    icon: Icon(Icons.play_arrow),
    iconSize: 50,
    color: Colors.white,
    onPressed: () {
        controller.play();
    },
)

ElevatedButton(
    child: Text('播放'),
    onPressed: () {},
)

4.3 容器 Widget

Container(
    width: 300,
    height: 300,
    margin: const EdgeInsets.all(10),
    padding: const EdgeInsets.all(10),
    decoration: BoxDecoration(
        color: Colors.grey[900],
        borderRadius: BorderRadius.circular(20),
    ),
    child: Text('内容'),
)

4.4 列表 Widget

ListView.builder(
    itemCount: controller.playlist.length,
    itemBuilder: (context, index) {
        final song = controller.playlist[index];
        return ListTile(
            leading: Icon(Icons.music_note),
            title: Text(song.title),
            subtitle: Text(song.artist),
            onTap: () => controller.playAt(index),
        );
    },
)

5. 布局 Widget(核心)

5.1 Column(垂直布局)

Column(
    mainAxisAlignment: MainAxisAlignment.center,
    crossAxisAlignment: CrossAxisAlignment.center,
    children: [
        Icon(Icons.music_note),
        Text('歌名'),
        Text('歌手'),
    ],
)

// MainAxisAlignment:center / start / end / spaceBetween / spaceAround / spaceEvenly

5.2 Row(水平布局)

Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
        IconButton(icon: Icon(Icons.skip_previous)),
        IconButton(icon: Icon(Icons.play)),
        IconButton(icon: Icon(Icons.skip_next)),
    ],
)

5.3 Stack(层叠布局)

Stack(
    children: [
        Image.network(coverUrl),
        Positioned(
            bottom: 0,
            left: 0,
            right: 0,
            child: Container(
                color: Colors.black54,
                child: Text('歌曲名'),
            ),
        ),
    ],
)

5.4 Expanded(占满剩余空间)

Row(
    children: [
        Text('左'),
        Expanded(child: Text('中间占满')),
        Text('右'),
    ],
)

6. 实战:写一个简单播放页面

import 'package:flutter/material.dart';

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

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(title: Text('播放器')),
            body: Column(
                children: [
                    // 封面
                    Container(
                        width: 250,
                        height: 250,
                        margin: const EdgeInsets.only(top: 40),
                        decoration: BoxDecoration(
                            color: Colors.grey[800],
                            borderRadius: BorderRadius.circular(20),
                        ),
                        child: const Icon(
                            Icons.music_note,
                            size: 100,
                            color: Colors.white54,
                        ),
                    ),

                    const SizedBox(height: 30),

                    // 歌曲信息
                    const Text(
                        '晴天',
                        style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
                    ),
                    const Text('周杰伦', style: TextStyle(fontSize: 16, color: Colors.grey)),

                    const SizedBox(height: 50),

                    // 进度条
                    Padding(
                        padding: const EdgeInsets.symmetric(horizontal: 20),
                        child: Column(
                            children: [
                                Container(
                                    height: 4,
                                    decoration: BoxDecoration(
                                        color: Colors.grey[700],
                                        borderRadius: BorderRadius.circular(2),
                                    ),
                                ),
                                const SizedBox(height: 5),
                                Row(
                                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                                    children: [
                                        Text('0:00', style: TextStyle(color: Colors.grey[600])),
                                        Text('4:29', style: TextStyle(color: Colors.grey[600])),
                                    ],
                                ),
                            ],
                        ),
                    ),

                    const SizedBox(height: 30),

                    // 控制按钮
                    Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                            IconButton(icon: const Icon(Icons.skip_previous), iconSize: 50),
                            IconButton(icon: const Icon(Icons.play_circle_filled), iconSize: 70),
                            IconButton(icon: const Icon(Icons.skip_next), iconSize: 50),
                        ],
                    ),
                ],
            ),
        );
    }
}

7. Widget 嵌套树

Scaffold
└── body: Column
    ├── children: [
    │   ├── Container (封面)
    │   ├── Text (歌名)
    │   ├── Text (歌手)
    │   ├── Column (进度条)
    │   │   ├── Container (进度条背景)
    │   │   └── Row (时间显示)
    │   └── Row (控制按钮)
    │       ├── IconButton (上一首)
    │       ├── IconButton (播放/暂停)
    │       └── IconButton (下一首)
    ]

类比 Java Swing:

JFrame
└── JPanel (contentPane)
    └── BoxLayout (Y轴)
        ├── JLabel (封面)
        ├── JLabel (歌名)
        └── JButton (播放按钮)

8. StatelessWidget vs StatefulWidget

// StatelessWidget = 静态组件,不会变化
class StaticButton extends StatelessWidget {
    const StaticButton({super.key});

    @override
    Widget build(BuildContext context) {
        return ElevatedButton(
            child: Text('点我'),
            onPressed: () {},
        );
    }
}

// StatefulWidget = 动态组件,会变化
class CounterWidget extends StatefulWidget {
    const CounterWidget({super.key});

    @override
    State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
    int _count = 0;

    @override
    Widget build(BuildContext context) {
        return Column(
            children: [
                Text('计数: $_count'),
                ElevatedButton(
                    child: Text('点我'),
                    onPressed: () {
                        setState(() {
                            _count++;
                        });
                    },
                ),
            ],
        );
    }
}

Vexfy 用的是 GetX,不直接用 StatefulWidget(GetX 的响应式自动处理状态变化)。


9. 速查表

Widget 作用 类比
Text 显示文本 JLabel
IconButton 图标按钮 JButton
ElevatedButton 文字按钮 JButton
Container 通用容器 div / JPanel
Column 垂直排列 flex column
Row 水平排列 flex row
Stack 层叠布局 FrameLayout
Expanded 占满剩余 flex: 1
ListView 滚动列表 JList
Padding 内边距 margin/padding
Scaffold 页面骨架 Activity
GetView GetX页面 @Controller

下一步