Flutter初识(3):Flutter开发初体验
一、创建Flutter项目
(1)安装好 VS Code 之后,打开进入欢迎页面,按下快捷键 Control + Shift + P,打开 命令面板 (Command Palette),如下图所示:
(2)选择 Flutter: New Project,就会创建一个新的工程,然后选择工程所在的文件夹;最后就是输入工程名,这里修改为 "first_project"(工程名要求全是小写字母):
(3)然后等待一会,就可以看到一个 main.dart 的文件,如下图所示:
(4)然后按下 F5 运行,第一次要你选择运行环境,这里选择 Windows 的环境下运行:
(5)然后运行程序,如下图所示:
二、工程目录分析
不管通过什么方式创建 flutter 工程,目录都是一样的:
目录中只有如下几个值得注意,其他不用管:
🟠android:存放 android 平台相关代码
🟡ios:存放 ios 平台相关代码
🟢lib:flutter 代码,即 dart 文件(我们编写的代码存放位置)
🔵test:存放测试代码
🟣pubspec.yaml:配置文件,存放一些第三方的依赖资源
我们在学习 flutter 入门,其实也可以不用管前两个,我们重点放在lib文件夹和pubspec.yaml文件,也就是存放我们代码编辑的位置。
pubspec.yaml文件
.yaml
文件是一个标记性语言,它类似于 JSON、XML
在flutter项目中pubspec.yaml
文件是用来进行包管理的,在我们需要引入第三方库或者插件时候,就可以放在这个文件夹中,类似于VUE、Node项目中的package.json
文件。但是两者引包方式是有区别的:
- node中通过
npm install
方式下载包 - flutter中通过
flutter packages get
方式下载包
pubspec.yaml
文件默认配置如下:
# 项目名称:pg_demo_test(*)
name: pg_demo_test
# 项目描述信息
description: A new Flutter project.
publish_to: 'none'
# 应用版本信息
version: 1.0.0+1
# 添加flutter和dart版本控制
environment:
sdk: ">=2.16.1 <3.0.0"
# 依赖包(所有依赖会编译到项目中)
dependencies:
flutter:
sdk: flutter
# 依赖包(运行期间的包)
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^1.0.0
# flutter相关配置
flutter:
uses-material-design: true
这里面的配置项有很多,这里针对几个重要的再做一个简单的描述和注意项:
🔴name配置项
name配置项是项目app应用名称,作用就在用于引入其他dart文件时需要使用,比如:
import 'package:pg_demo_test/libo/gf_button.dart';
这里pg_demo_test就是此应用名称,🔺注意:当你name字段修改,所有引入文件对应的名字也要更改
🟠environment配置项
该配置项对flutter和dart进行了版本控制
environment:
sdk: ">=2.16.1 <3.0.0"
这里表示该应用只能在高于或大于2.16.1,低于3.0.0的SDK上运行,当然我们也可以自己更更改它。
🟡dependencies 和 dev_dependencies配置项
这两个配置项都是该应用程序所需要的以来包、库,区别在于:
- dependencies下面的依赖包会编译到项目中
- dev_dependencies下面的依赖包仅用于运行期间
这里的依赖包来源可以是:
pub.dev 网站上的第三方库
dependencies:
flutter:
sdk: flutter
getwidget: ^2.0.5
animated_background: ^2.0.0
cupertino_icons: ^1.0.2
这三个就是引用的第三方库。需要注意的是空格缩进
git安装
dependencies:
libo:
git:
url: git://github.com/libo/test.git
本地库
dependencies:
my_package:
path: ../my_package
自己封装的库
dependencies:
flutter:
sdk: flutter
bloc:
hosted:
name: test
url: http....
version: ^0.0.0
🟢flutter配置项
默认有一个 uses-material-design: true 配置,可以使用Material中资源,根据注释信息,它还有assets
和plugin
字段:
flutter:
uses-material-design: true
assets:
plugin:
assets
用于配置静态资源(图片、字体...)
图片配置:
flutter:
uses-material-design: true
assets:
- images/1.png # images下面1.png资源
- images # images路径下所有资源
字体配置:
flutter:
uses-material-design: true
assets:
- family: family_name
fonts:
- asset:fonts/aa.ttf
- asset:fonts/bb.ttf
family 与 fonts 是一个整体,列表的一项
三、模板代码分析
默认项目分析:
- 工程目录下有一个
lib
文件夹,里面会存放我们编写的 Flutter 代码; - 打开发现里面有一个
main.dart
,它是我们Flutter启动的入口文件
,里面有main函数
;
默认代码分析:
- 这是一个计数器的案例程序,点击右下角的
+
符号,上面显示的数字会递增; - 但是我们第一次接触 main.dart 中的代码,可能会发现很多
不认识
的代码,不知道这个内容是如何编写出来的;
作为初学者,我的建议是将其中所有的代码全部删除掉,从零去创建里面的代码,这样我们才能对Flutter应用程序的结构非常清晰;
在这个示例中,主要Dart代码是在 lib/main.dart 文件中,下面是它的源码(删除默认注释后的版本):
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
四、开始Flutter代码
4.1 Hello World
做任何的开发,我们都是从祖传的Hello World
开始,那么现在我们的需求来了:
- 在界面中心位置,显示一个Hello World;
下面,我们就动手开始编写 Hello World:
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
main(List<String> args) {
runApp(Text("Hello World", textDirection: TextDirection.ltr));
}
也可以改写成箭头函数:
void main()=>runApp(Text("Hello World", textDirection: TextDirection.ltr));
当然,上面的代码我们已经实现了在界面上显示Hello World:
- 但是
没有居中
,字体也有点小
; - 这些问题,我们放到后面再来解决,先搞懂目前的几行代码;
上面的代码我们有一些比较熟悉,有一些并不清楚是什么:
- 比如我们知道 Dart 程序的
入口都是main函数
,而 Flutter 是 Dart 编写的,所以入口也是 main 函数; - 但是我们导入的
Material是什么呢
? - 另外,我们在main函数中调用了一个
runApp()函数
又是什么呢? - 备注:第一行的
// ignore_for_file: prefer_const_constructors
,是为了忽略提示Prefer const with constant constructors
,否则有些不加 const 的代码行下面会有蓝色波浪线;
下面,我们对不认识的代码进行一些分析。
4.2 代码分析
(1)runApp 和 Widget
runApp是 Flutter 内部提供的一个函数,当我们启动一个 Flutter 应用程序时就是从调用这个函数开始的
- 我们可以点到 runApp 的实现源码,查看到该函数
- 我们暂时不分析具体的源码(因为我发现过多的理论,对于初学者来说并不友好)
void runApp(Widget app) {
...省略代码
}
该函数让我们传入一个东西:Widget。
我们先说Widget的翻译:
- Widget 在国内有很多的翻译;
- 做过 Android、iOS 等开发的人群,喜欢将它翻译成控件;
- 做过 Vue、React 等开发的人群,喜欢将它翻译成组件;
- 如果我们使用 Google,Widget 翻译过来应该是小部件;
- 没有说哪种翻译一定是对的,或者一定是错的,但是我个人更倾向于小部件或者组件;
Widget 到底什么东西呢?
- 我们学习 Flutter,从一开始就可以有一个基本的认识:Flutter 中万物皆 Widget;
- 在我们 iOS 或者 Android 开发中,我们的界面有很多种类的划分:应用(Application)、视图控制器(View Controller)、活动(Activity)、View(视图)、Button(按钮)等等;
- 但是在 Flutter 中,这些东西都是不同的 Widget 而已;
- 也就是我们整个应用程序中
所看到的内容
几乎都是 Widget,甚至是内边距的设置
,我们也需要使用一个叫Padding 的 Widget
来做;
runApp 函数让我们传入的就是一个 Widget:
- 但是我们现在没有 Widget,怎么办呢?
- 我们可以导入 Flutter 默认已经给我们提供的 Material 库,来使用其中的很多内置 Widget;
(2)Material 设计风格
material是什么呢?
- material 是 Google 公司推行的一套
设计风格
,或者叫设计语言
、设计规范
等; - 里面有非常多的设计规范,比如
颜色
、文字的排版
、响应动画与过度
、填充
等等; - 在 Flutter 中高度集成了
Material风格的Widget
; - 在我们的应用中,我们可以直接使用这些 Widget 来创建我们的应用(后面会用到很多);
Text 小部件分析:
- 我们可以使用 Text 小部件来完成文字的显示;
- 我们发现 Text 小部件继承自 StatelessWidget,StatelessWidget 继承自 Widget;
- 所以我们可以将 Text 小部件传入到 runApp 函数中
- 属性非常多,但是我们已经学习了 Dart 语法,所以你会发现只有 this.data 属性是必须传入的。
class Text extends StatelessWidget {
const Text(
this.data, {
Key key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
this.textWidthBasis,
});
}
4.3 代码改进
(1)改进界面样式
我们发现现在的代码并不是我们想要的最终结果:
- 我们可能希望文字居中显示,并且可以大一些;
- 居中显示: 需要使用另外一个 Widget,
Center
; - 文字大一些: 需要给 Text 文本设置一些样式;
我们修改代码如下:
- 我们在 Text 小部件外层包装了一个 Center 部件,让 Text 作为其 child;
- 并且,我们给 Text 组件设置了一个属性:style,对应的值是 TextStyle 类型;
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
main(List<String> args) {
runApp(Center(
child: Text(
"Hello World",
textDirection: TextDirection.ltr,
style: TextStyle(fontSize: 36),
),
));
}
(2)改进界面结构
目前我们虽然可以显示 HelloWorld,但是我们发现最底部的背景是黑色,并且我们的页面并不够结构化。
- 正常的 App 页面应该有一定的结构,比如通常都会有
导航栏
,会有一些背景颜色
等
在开发当中,我们并不需要从零去搭建这种结构化的界面,我们可以使用 Material 库,直接使用其中的一些封装好的组件来完成一些结构的搭建。
我们通过下面的代码来实现:
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
main(List<String> args) {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text("CODERWHY"),
),
body: Center(
child: Text(
"Hello World",
textDirection: TextDirection.ltr,
style: TextStyle(fontSize: 36),
),
),
),
));
}
在最外层包裹一个MaterialApp
- 这意味着整个应用我们都会采用 MaterialApp 风格的一些东西,方便我们对应用的设计,并且目前我们使用了其中两个属性;
- title:这个是定义在 Windows 系统中打开多任务切换窗口时显示的标题;(暂时可以不写)
- home:是该应用启动时显示的页面,我们传入了一个 Scaffold;
Scaffold是什么呢?
- 翻译过来是
脚手架
,脚手架的作用就是搭建页面的基本结构; - 所以我们给 MaterialApp 的 home 属性传入了一个 Scaffold 对象,作为启动显示的 Widget;
- Scaffold 也有一些属性,比如
appBar
和body
; - appBar 是用于设计导航栏的,我们传入了一个
title属性
; - body 是页面的内容部分,我们传入了之前已经创建好的 Center 中包裹的一个 Text 的 Widget;
(3)进阶案例实现
我们可以让界面中存在更多的元素:
- 写到这里的时候,你可能已经发现
嵌套太多
了,不要着急,我们后面会对代码重构的
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
main(List<String> args) {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("CODERWHY"),
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Checkbox(value: true, onChanged: (value) => print("Hello World")),
Text(
"同意协议",
textDirection: TextDirection.ltr,
style: TextStyle(fontSize: 20),
)
],
),
),
),
));
}
4.4 代码重构
(1)创建自己的Widget
很多学习 Flutter 的人,都会被 Flutter 的嵌套
劝退,当代码嵌套过多时,结构很容易看不清晰。
这里有两点我先说明一下:
- Flutter 整个开发过程中就是形成一个 Widget 树,所以形成嵌套是很正常的。
- 关于 Flutter 的代码缩进,更多开发中我们使用的是 2 个空格(前端开发 2 个空格居多,你喜欢 4 个也没问题)
但是,我们开发一个这么简单的程序就出现如此多的嵌套,如果应用程序更复杂呢?
- 我们可以对我们的代码进行封装,将它们封装到自己的 Widget 中,创建自己的 Widget;
如何创建自己的 Widget 呢?
- 在 Flutter 开发中,我们可以继承自 StatelessWidget 或者 StatefulWidget 来创建自己的 Widget 类;
- StatelessWidget: 没有状态改变的 Widget,通常这种 Widget 仅仅是做一些展示工作而已;
- StatefulWidget: 需要保存状态,并且可能出现状态改变的Widget;
在上面的案例中对代码的重构,我们使用 StatelessWidget 即可,所以我们接下来学习一下如果利用 StatelessWidget 来对我们的代码进行重构;
StatefulWidget 我们放到后面的一个案例中来学习;
(2)StatelessWidget
StatelessWidget 通常是一些没有状态(State,也可以理解成 data)需要维护的 Widget:
- 它们的数据通常是直接写死(放在 Widget 中的数据,必须被定义为 final,为什么呢?我在下一篇博客讲解 StatefulWidget 会讲到);
- 从 parent widget中 传入的而且一旦传入就不可以修改;
- 从 InheritedWidget 获取来使用的数据(这个放到后面会讲解);
我们来看一下创建一个 StatelessWidget 的格式:
- 让自己创建的 Widget 继承自 StatelessWidget;
- StatelessWidget 包含一个必须重写的方法:build 方法;
class MyStatelessWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return <返回我们的Widget要渲染的Widget,比如一个Text Widget>;
}
}
build 方法的解析:
- Flutter 在拿到我们自己创建的 StatelessWidget 时,就会执行它的 build 方法;
- 我们需要在 build 方法中告诉 Flutter,我们的 Widget 希望渲染什么元素,比如一个 Text Widget;
- StatelessWidget 没办法主动去执行 build 方法,当我们使用的数据发生改变时,build 方法会被重新执行;
build 方法什么情况下被执行呢?:
- 当我们的 StatelessWidget 第一次被插入到 Widget 树中时(也就是第一次被创建时);
- 当我们的父 Widget(parent widget)发生改变时,子 Widget 会被重新构建;
- 如果我们的 Widget 依赖 InheritedWidget 的一些数据,InheritedWidget 数据发生改变时;
(3)重构案例代码
现在我们就可以通过 StatelessWidget 来对我们的代码进行重构了
- 因为我们的整个代码都是一些数据展示,没有数据的改变,使用 StatelessWidget 即可;
- 另外,为了体现更好的封装性,我对代码进行了两层的拆分,让代码结构看起来更加清晰;(具体的拆分方式,我会在后面的案例中不断的体现出来,目前我们先拆分两层)
重构后的代码如下:
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
main(List<String> args) {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("CODERWHY"),
),
body: HomeContent(),
),
);
}
}
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Checkbox(value: true, onChanged: (value) => print("Hello World")),
Text(
"同意协议",
textDirection: TextDirection.ltr,
style: TextStyle(fontSize: 20),
)
],
),
);
}
}
五、案例练习
5.1 案例最终效果
我们先来看一下案例的最终展示效果:
- 这个效果中我们会使用很多没有接触的 Widget;
- 没有关系,后面这些常用的 Widget 我会一个个讲解;
- 这个案例最主要的目的还是让大家更加熟悉 Flutter 的开发模式以及自定义 Widget 的封装过程;
5.2 自定义Widget
在我们的案例中,很明显一个产品的展示就是一个大的 Widget,这个 Widget 包含如下 Widget:
- 标题的 Widget:使用一个Text Widget完成;
- 描述的 Widget:使用一个Text Widget完成;
- 图片的 Widget:使用一个Image Widget完成;
- 上面三个 Widget 要垂直排列,我们可以使用一个 Column 的 Widget
另外,三个展示的标题、描述、图片都是不一样的,所以我们可以让 Parent Widget 来决定内容:
- 创建三个成员变量保存父 Widget 传入的数据
class ProductItem extends StatelessWidget {
final String title;
final String desc;
final String imageURL;
ProductItem(this.title, this.desc, this.imageURL);
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text(title, style: TextStyle(fontSize: 24)),
Text(desc, style: TextStyle(fontSize: 18)),
Image.network(imageURL)
],
);
}
}
5.3 列表数据展示
现在我们就可以创建三个 ProductItem 来让他们展示了:
- MyApp 和上面是一致的,没有任何改变;
- HomeContent 中,我们使用了一个 Column,因为我们创建的三个 ProductItem 是垂直排列的
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
main(List<String> args) {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("CODERWHY"),
),
body: HomeContent(),
),
);
}
}
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
ProductItem("Apple1", "Macbook Product1",
"https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg"),
ProductItem("Apple2", "Macbook Product2",
"https://tva1.sinaimg.cn/large/006y8mN6gy1g72imm9u5zj30u00k0adf.jpg"),
ProductItem("Apple3", "Macbook Product3",
"https://tva1.sinaimg.cn/large/006y8mN6gy1g72imqlouhj30u00k00v0.jpg"),
],
);
}
}
class ProductItem extends StatelessWidget {
final String title;
final String desc;
final String imageURL;
ProductItem(this.title, this.desc, this.imageURL);
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text(title, style: TextStyle(fontSize: 24)),
Text(desc, style: TextStyle(fontSize: 18)),
Image.network(imageURL)
],
);
}
}
运行效果如下:
- 错误信息:下面出现了黄色的斑马线;
- 这是因为在 Flutter 的布局中,内容是不能超出屏幕范围的,当超出时不会自动变成滚动效果,而是会报下面的错误;
如何可以解决这个问题呢?
- 我们将 Column 换成 ListView 即可;
- ListView 可以让自己的子 Widget 变成滚动的效果;
5.4 案例细节调整
(1)界面整体边距
如果我们希望整个内容距离屏幕的边缘有一定的间距,怎么做呢?
- 我们需要使用另外一个 Widget:Padding,它有一个 padding 属性用于设置边距大小;
- 没错,设置内边距也是使用 Widget,这个 Widget 就是Padding;
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
children: <Widget>[
ProductItem("Apple1", "Macbook Product1",
"https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg"),
ProductItem("Apple2", "Macbook Product2",
"https://tva1.sinaimg.cn/large/006y8mN6gy1g72imm9u5zj30u00k0adf.jpg"),
ProductItem("Apple3", "Macbook Product3",
"https://tva1.sinaimg.cn/large/006y8mN6gy1g72imqlouhj30u00k00v0.jpg"),
],
),
);
}
}
(2)商品内边距和边框
我们现在希望给所有的商品也添加一个内边距,并且还有边框,怎么做呢?
- 我们可以使用一个 Container 的 Widget,它里面有 padding 属性,并且可以通过 decoration 来设置边框;
- Container 我们也会在后面详细来讲,我们先用起来;
class ProductItem extends StatelessWidget {
final String title;
final String desc;
final String imageURL;
ProductItem(this.title, this.desc, this.imageURL);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(border: Border.all()),
child: Column(
children: <Widget>[
Text(title, style: TextStyle(fontSize: 24)),
Text(desc, style: TextStyle(fontSize: 18)),
Image.network(imageURL)
],
),
);
}
}
(3)文字图片的间距
我们希望给图片和文字之间添加一些间距,怎么做呢?
- 方式一:给图片或者文字添加一个向上的内边距或者向下的内边距;
- 方式二:使用 SizedBox 的 Widget,设置一个 height 属性,可以增加一些距离;
class ProductItem extends StatelessWidget {
final String title;
final String desc;
final String imageURL;
ProductItem(this.title, this.desc, this.imageURL);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(border: Border.all()),
child: Column(
children: <Widget>[
Text(title, style: TextStyle(fontSize: 24)),
Text(desc, style: TextStyle(fontSize: 18)),
SizedBox(
height: 10,
),
Image.network(imageURL)
],
),
);
}
}
5.5 最终实现代码
最后,我给出最终实现代码:
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
main(List<String> args) {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("CODERWHY"),
),
body: HomeContent(),
),
);
}
}
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
children: <Widget>[
ProductItem("Apple1", "Macbook Product1",
"https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg"),
ProductItem("Apple2", "Macbook Product2",
"https://tva1.sinaimg.cn/large/006y8mN6gy1g72imm9u5zj30u00k0adf.jpg"),
ProductItem("Apple3", "Macbook Product3",
"https://tva1.sinaimg.cn/large/006y8mN6gy1g72imqlouhj30u00k00v0.jpg"),
],
),
);
}
}
class ProductItem extends StatelessWidget {
final String title;
final String desc;
final String imageURL;
ProductItem(this.title, this.desc, this.imageURL);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(border: Border.all()),
child: Column(
children: <Widget>[
Text(title, style: TextStyle(fontSize: 24)),
Text(desc, style: TextStyle(fontSize: 18)),
SizedBox(
height: 10,
),
Image.network(imageURL)
],
),
);
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库