dart官方文档:www.dartcn.com/
先放上demo的github地址:github.com/MonkeyInWin…这篇笔记中的demo都在这个项目里。
前边学了一些简单的组件下面从0开始来写个demo。
动手之前先看demo效果
一、创建一个项目
在之前的笔记里(flutter笔记(一)-----官方示例&代码解读)已经说过怎么创建项目,以及官方demo的解读,这里就不详细说了。
flutter create flutter_demo
看到控制台打印以下信息,说明创建成功了。
Android Studio
二、写Demo
1、一段文本
Project,打开lib目录下的main.dart,删除大部分代码,只保留以下部分:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
);
}
}
接下来在MaterialApp中添加文本。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Text('我是一段文本')
);
}
}
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold( //翻译成中文就是脚手架,提供了一个布局框架,里边有很多常用的api,比如顶部标题、底部菜单、左右抽屉等。
appBar: AppBar(
title: Text('文本')
),
body: Text('我是一段文本')
)
);
}
}
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('文本')
),
body: Center(
child: Text(
'我是一段文本',
style: TextStyle(
color: Color.fromARGB(0xFF, 0xFF, 0x11, 0xF5)
)
)
)
)
);
}
}
只需要加上Center这个widget,就实现了水平垂直居中。
关于Text的详细介绍看这里flutter笔记(二)-----hello world和文本组件Text、TextSpan
到这可能有人会提出问题,一个app不可能所有代码都放在一个class里,那根本没法看,这就是接下来要干的事。
2、组件封装
定义一个新的class叫Page1,并把scaffold放在里边。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Page1()
);
}
}
class Page1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page1')
),
body: Center(
child: Text(
'我是一段文本',
style: TextStyle(
color: Color.fromARGB(0xFF, 0xFF, 0x11, 0xF5)
)
)
)
);
}
}
是不是感觉像是在写react(不考虑他这蛋疼的写法)。
写到这又会有人提出问题,这是没写到同一个class里,但是是在同一个文件里啊,接下来咱们就拆成两个文件。
在lib目录下新建一个文件叫page1.dart。
import 'package:flutter/material.dart';
class Page1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page1')
),
body: Center(
child: Text(
'我是一段文本',
style: TextStyle(
color: Color.fromARGB(0xFF, 0xFF, 0x11, 0xF5)
)
)
)
);
}
}
就是把material.dartimport进来之后再把刚刚main.dart里Page1粘过来。
下面改写main.dart,将刚新建的page1.dartimport进来。
import 'package:flutter/material.dart';
import 'package:flutter_demo/page1.dart'; //项目目录名/文件名
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Page1(),
);
}
}
这样就做到了组件拆分。
开篇的demo地址切到demo_1分支,就是上边的完整代码。
3、一个方块
html有个最常用的标签div,曾有一段时间把页面布局叫做div布局,flutter里有个类似的widget叫Container(flutter笔记(三)-----容器组件Container)。
接下来我们在lib目录下新建一个page2.dart文件。
整体框架和page1.dart相同。
import 'package:flutter/material.dart';
class Page2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page2')
),
body: Center(
)
);
}
}
我们在main.dart里把page2.dartimport进来,然后把home改成Page2。
import 'package:flutter/material.dart';
//import 'package:flutter_demo/page1.dart';
import 'package:flutter_demo/page2.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Page2(),
);
}
}
page2,接下来在Center里边写一个Container。
import 'package:flutter/material.dart';
class Page2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page2')
),
body: Center(
child: Container(
width: 200,
height: 200,
color: Colors.red
)
)
);
}
}
可以看见页面上出现了一个红色的方块。
有page1和page2了,接下来看以下页面怎么跳转。
4、路由
我们来改造一下前边写的demo,main.dart还是importpage1.dart。
import 'package:flutter/material.dart';
import 'package:flutter_demo/page1.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Page1(),
);
}
}
在page1.dart中,我们在右下角加一个悬浮按钮。
import 'package:flutter/material.dart';
class Page1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page1')
),
body: Center(
child: Text(
'我是一段文本',
style: TextStyle(
color: Color.fromARGB(0xFF, 0xFF, 0x11, 0xF5)
)
)
),
floatingActionButton: FloatingActionButton(
onPressed: () {
print('pressed next page');
}
)
);
}
}
这个时候右下角出现了一个蓝色的悬浮按钮,点击之后可以看见控制台输出打印的信息。
floatingActionButton: FloatingActionButton(
onPressed: () {
print('pressed next page');
},
child: new Icon(Icons.arrow_forward),
)
效果如下:
先在
page1.dart中将page2.dartimport一下,然后写路由跳转,page1.dart完整的代码如下:
import 'package:flutter/material.dart';
import 'package:flutter_demo/page2.dart';
class Page1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page1')
),
body: Center(
child: Text(
'我是一段文本',
style: TextStyle(
color: Color.fromARGB(0xFF, 0xFF, 0x11, 0xF5)
)
)
),
floatingActionButton: FloatingActionButton(
onPressed: () {
print('pressed next page');
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => Page2()),
);
},
child: new Icon(Icons.arrow_forward),
)
);
}
}
效果如下
将
page2.dart中的Container删除,换成一个按钮MaterialButton(flutter笔记(六)-----按钮 各种Button
)。
代码如下
import 'package:flutter/material.dart';
class Page2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page2')
),
body: Center(
child: MaterialButton(
child: Text('back'),
color: Colors.blue,
onPressed: () {
print('back');
Navigator.pop(context);
}
)
)
);
}
}
Navigator.pop(context)方法就可以返回上一页。分支切换到
demo_2为以上demo的代码。
5、插播一条调试
布局
写web都知道开发者工具可以定位到页面上任意一个元素,flutter也可以。
Android Studio在菜单栏View -> Tool Windows -> Flutter Inspector。
打开之后在编辑区右侧出现了调试工具。
Widgets可以看见整个页面的结构,点左上角的准星,可以去模拟器中选中某一个Widget。
Widget上的所有属性和样式同时模拟器左下角还会出现一个放大镜,点击放大镜后可以再选中其他Widget。
打断点
需要在debug模式下运行才可以打断点。
back这个按钮。
利用浏览器调试
Dart DevTools,和在Android Studio调试基本相同,就不重复了,放一张图。
6、图片
首先新建一个page3.dart的文件,在page2.dart中添加floatingActionButton,并将page3.dartimport进来,page3.dart 中搭好页面。
page2.dart
import 'package:flutter/material.dart';
import 'package:flutter_demo/page3.dart';
class Page2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page2')
),
body: Center(
child: MaterialButton(
child: Text('back'),
color: Colors.blue,
onPressed: () {
print('back');
Navigator.pop(context);
}
)
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.arrow_forward),
onPressed: () {
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => Page3()),
);
}
),
);
}
}
page3.dart
import 'package:flutter/material.dart';
class Page3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page3')
),
body: Center(
),
);
}
}
在第二页点右下角的按钮会进入第三页, 一个空页面。
接下来在根目录下新建一个images文件夹,里边放一张图片。
把刚才图片的路径添加到pubspec.yaml。
接下来就可以使用这张图片了。
import 'package:flutter/material.dart';
class Page3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page3')
),
body: Center(
child: Image.asset('./images/logo.png')
),
);
}
}
保存之后page3中会居中显示一个flutter的logo。
有时候我们不需要把图片打包进来,需要用到网络图片,这个时候需要把Image.asset换成Image.network。
import 'package:flutter/material.dart';
class Page3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page3')
),
body: Center(
child: Image.network('https://www.baidu.com/img/bd_logo1.png')
),
);
}
}
去网上复制一张图片的链接,如果图片不显示,有可能是图片加了防盗链,这里用了百度的logo。
网络图片都有个加载时间,我们在放一个loading占位。
把Center换成Stack,Stack里放两个Center,再把loading和图片放在Center里。
import 'package:flutter/material.dart';
class Page3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page3')
),
body: Stack(
children: <Widget>[
Center(
child: CircularProgressIndicator()
),
Center(
child: Image.network('https://www.baidu.com/img/bd_logo1.png')
),
],
),
);
}
}
这里简单介绍一下Stack,类似于position: relative,子Widget会重叠显示,上边的demo实际上是图片加载之后把loading盖住了。
上边虽然实现了loading占位,但是图片显示太过生硬,我们用FadeInImage给他加个淡入效果。
pubspec.yaml里添加一个transparent_image(github.com/brianegan/t…
)在图片加载之前占位用,这里其实体现不出来他的作用,但是placeholder不能为空。
这里用了第三方的包pub.dev/,类似于npm的仓库。
命令行执行flutter pub get如果是在Android Studio中添加之后会有提示,安装好之后改写一下page3.dart。
import 'package:flutter/material.dart';
import 'package:transparent_image/transparent_image.dart';
class Page3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page3')
),
body: Stack(
children: <Widget>[
Center(
child: CircularProgressIndicator()
),
Center(
child: FadeInImage.memoryNetwork(
placeholder: kTransparentImage,
image: 'https://www.baidu.com/img/bd_logo1.png'
)
),
],
),
);
}
}
效果如下
这就比直接显示图片要好很多。
以上代码在demo_3分支。
7、滚动列表 & 网格布局
还是在page3.dart中添加floatingActionButton,然后新建一个文件page4.dart,在page3.dart中import。
先看一个最简单的列表
import 'package:flutter/material.dart';
class Page4 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page4')
),
body: ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.phone),
title: Text('Title1')
),
ListTile(
leading: Icon(Icons.cached),
title: Text('Title2')
),
ListTile(
leading: Icon(Icons.adb),
title: Text('Title3')
),
ListTile(
leading: Icon(Icons.adjust),
title: Text('Title4')
),
],
)
);
}
}
这样就实现了一个滚动列表,可以多复制一些ListTitle尝试一下上下滑动,这里就不写了。
纵向滚动实现里,下面看一下横向滚动,直接在ListView里加一个Container。
import 'package:flutter/material.dart';
class Page4 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page4')
),
body: ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.phone),
title: Text('Title1')
),
ListTile(
leading: Icon(Icons.cached),
title: Text('Title2')
),
Container(
height: 200,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
Container(
color: Colors.red,
width: 150,
),
Container(
color: Colors.blue,
width: 150,
),
Container(
color: Colors.green,
width: 150,
),
Container(
color: Colors.yellow,
width: 150,
),
]
)
)
],
)
);
}
}
这还不满足的话继续往下看,我们还可以在列表里同时展示两列。
import 'package:flutter/material.dart';
class Page4 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page4')
),
body: ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.phone),
title: Text('Title1')
),
ListTile(
leading: Icon(Icons.cached),
title: Text('Title2')
),
Container(
height: 200,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
Container(
color: Colors.red,
width: 150,
),
Container(
color: Colors.blue,
width: 150,
),
Container(
color: Colors.green,
width: 150,
),
Container(
color: Colors.yellow,
width: 150,
),
]
)
),
Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(width: 1, color: Colors.black)
),
child: GridView.count(
crossAxisCount: 2,
children: List.generate(100, (index) {
return Center(
child: Text('Item $index')
);
})
)
)
],
)
);
}
}
这里用了GridView和List.generate。
GridView就是网格布局,可以指定一行有几个Widget,每个Widget之间的距离等。
List.generate值是一个函数,返回一个Widget,用来生成一组Widget。
以上代码在demo_4分支。
8、手势
前边都是一些常见Widget的简单用法,接下来说一下手势。
先来介绍一下Pointers,用户与屏幕交互的原始数据,包括PointerDownEvent、PointerMoveEvent、PointerUpEvent、PointerCancelEvent。类似于移动端web的touch事件。
再说一下手势,一个或几个Pointer的封装,先来一个按钮的demo看一下。
import 'package:flutter/material.dart';
class Page5 extends StatelessWidget{
final items = new List<String>.generate(5, (i) => 'Item ${i + 1}');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page5'),
),
body: InkWell(
child: Center(
child: Container(
child: Text('this is a button'),
padding: EdgeInsets.only(
top: 10,
bottom: 10
)
)
),
onTap: () {
print('on tap');
},
onTapDown: (tapDownDetail) {
print('on tap down');
},
onTapCancel: () {
print('on tap cancel');
},
onDoubleTap: () {
print('on dubble tap');
},
onLongPress: () {
print('on long press');
}
)
);
}
}
这样一个全屏的大按钮就完成了,点击还有水波纹效果。。。
监听了五种手势,可以看一下打印。
接下来对demo进行改造,写一个可以滑动删除的列表。
import 'package:flutter/material.dart';
class Page5 extends StatelessWidget{
final items = new List<String>.generate(5, (i) => 'Item ${i + 1}');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page5'),
),
body: ListView(
children: <Widget> [
InkWell(
child: Center(
child: Container(
child: Text('this is a button'),
padding: EdgeInsets.only(
top: 10,
bottom: 10
)
)
),
onTap: () {
print('on tap');
},
onTapDown: (tapDownDetail) {
print('on tap down');
},
onTapCancel: () {
print('on tap cancel');
},
onDoubleTap: () {
print('on dubble tap');
},
onLongPress: () {
print('on long press');
}
),
Container(
height: 400,
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return Dismissible(
key: Key('key_$index'),
onDismissed: (direction) {
items.removeAt(index);
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('$item dismissed')
)
);
},
background: Container(
color: Colors.red
),
child: ListTile(
title: Text('$item')
)
);
}
)
)
]
)
);
}
}
看一下效果
List.generate方法生成了一个List ,然后用了ListView.builder生成了一个ListView。Dismissible是flutter提供的一个可以滑动删除的Widget,SnackBar就是底部的提示。关于手势的中文文档看这里flutterchina.club/gestures/。
以上demo在
demo_5分支。
9、有状态组件
前边的Widget都是继承StatelessWidget也就是无状态组件,接下来看一下StatefulWidget有状态组件。
先来布个局
Container,下边一行放三个按钮MaterialButton,最终实现的效果就是点击按钮改变上边Container的颜色。先看布局代码
import 'package:flutter/material.dart';
class Page6 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page6'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
width: 200,
height: 200,
color: Colors.red,
margin: EdgeInsets.only(
top: 20,
bottom: 20
),
),
Row (
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container (
padding: EdgeInsets.only(
left: 10,
right: 10
),
child: MaterialButton (
color: Colors.red,
child: Text('red'),
onPressed: () {
print('red');
},
),
),
Container (
padding: EdgeInsets.only(
left: 10,
right: 10
),
child: MaterialButton (
color: Colors.blue,
child: Text('blue'),
onPressed: () {
print('blue');
}
),
),
Container (
padding: EdgeInsets.only(
left: 10,
right: 10
),
child: MaterialButton (
color: Colors.green,
child: Text('green'),
onPressed: () {
print('green');
}
)
)
],
)
],
)
);
}
}
里边用了Column和Row这两个新的Widget,一个是子Widget纵向排列,另一个是横向排列,另外还用了MainAxisAlignment主轴上的对齐方式和CrossAxisAlignment交叉轴上的对齐方式,这里都用了居中,至于margin为什么不加在MaterialButton上,对不起,没有。
接下来对demo进行改写,StatelessWidget肯定是不行的,要换成StatefulWidget。
先看代码
import 'package:flutter/material.dart';
class Page6 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page6'),
),
body: BoxChangeColor()
);
}
}
class BoxChangeColor extends StatefulWidget {
@override
_BoxChangeColorState createState() => new _BoxChangeColorState();
}
class _BoxChangeColorState extends State<BoxChangeColor> {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
width: 200,
height: 200,
color: Colors.red,
margin: EdgeInsets.only(
top: 20,
bottom: 20
),
),
Row (
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container (
padding: EdgeInsets.only(
left: 10,
right: 10
),
child: MaterialButton (
color: Colors.red,
child: Text('red'),
onPressed: () {
print('red');
},
),
),
Container (
padding: EdgeInsets.only(
left: 10,
right: 10
),
child: MaterialButton (
color: Colors.blue,
child: Text('blue'),
onPressed: () {
print('blue');
}
),
),
Container (
padding: EdgeInsets.only(
left: 10,
right: 10
),
child: MaterialButton (
color: Colors.green,
child: Text('green'),
onPressed: () {
print('green');
}
)
)
],
)
],
);
}
}
这里新建了一个BoxChangeColor继承了StatefulWidget,并重写了createState方法,再创建一个_BoxChangeColorState类继承State,在_BoxChangeColorState返回上边的Column,这样就完成了一个缺少状态的有状态组件。
以_开头表示私有。
下面把缺少的状态添加进去。
在_BoxChangeColorState中声明一个变量,这个变量就是state,类型为Color并把这个state写成Container的 color属性值,在MaterialButton的onPressed事件中调用setState方法来改变state,这个时候会重新build,实现了切换颜色。
这个demo的完整带么如下:
import 'package:flutter/material.dart';
class Page6 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('page6'),
),
body: BoxChangeColor()
);
}
}
class BoxChangeColor extends StatefulWidget {
@override
_BoxChangeColorState createState() => new _BoxChangeColorState();
}
class _BoxChangeColorState extends State<BoxChangeColor> {
Color color = Colors.red;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
width: 200,
height: 200,
color: color,
margin: EdgeInsets.only(
top: 20,
bottom: 20
),
),
Row (
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container (
padding: EdgeInsets.only(
left: 10,
right: 10
),
child: MaterialButton (
color: Colors.red,
child: Text('red'),
onPressed: () {
setState(() {
color = Colors.red;
});
},
),
),
Container (
padding: EdgeInsets.only(
left: 10,
right: 10
),
child: MaterialButton (
color: Colors.blue,
child: Text('blue'),
onPressed: () {
setState(() {
color = Colors.blue;
});
}
),
),
Container (
padding: EdgeInsets.only(
left: 10,
right: 10
),
child: MaterialButton (
color: Colors.green,
child: Text('green'),
onPressed: () {
setState(() {
color = Colors.green;
});
}
)
)
],
)
],
);
}
}
再来看一下效果
demo_6分支。
10、模拟登录
前边都是基本用法的介绍,而且demo都很零碎,接下来做一个模拟登录的demo。
点击Login按钮跳转至登录页面,点击Cancel按钮返回登录页面,输入username和password后点击登录页面的Login按钮模拟登录,跳转回前一页面,这个时候隐藏登录页面的Login按钮并显示一张图片。
首先新建一个login.dart文件用作登录页面,终于不是page了,在里边创建一个名为Login的StatefulWidget,然后在main.dart中把这个文件import进来,然后在main.dart创建一个名为HomePage的StatefulWidget作为主页,中间放一个按钮,点击按钮跳转至登录页。
先看效果
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_demo/login.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => new _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold (
appBar: AppBar(
title: Text('Login')
),
body: Center (
child: RaisedButton(
child: Text('Login'),
color: Colors.blue,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
fullscreenDialog: true,
builder: (context) => Login()
)
);
}
)
)
);
}
}
这里用了个fullscreenDialog属性,全屏弹窗,下一个页面从底部弹出。
login.dart
import 'package:flutter/material.dart';
class Login extends StatefulWidget {
@override
_LoginState createState() => new _LoginState();
}
class _LoginState extends State<Login> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container (
padding: EdgeInsets.only(
top:50,
left: 20,
right: 20
),
child: TextField (
decoration: InputDecoration(
prefixIcon: Icon(Icons.person),
labelText: ('username')
),
)
),
Container (
padding: EdgeInsets.all(20),
child: TextField (
decoration: InputDecoration(
prefixIcon: Icon(Icons.lock),
labelText: ('password')
),
obscureText: true
)
),
Row (
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container (
margin: EdgeInsets.only(
left: 10,
right: 10
),
child: RaisedButton(
child: Text('Login'),
color: Colors.blue,
onPressed: () {
}
)
),
Container (
margin: EdgeInsets.only(
left: 10,
right: 10
),
child: RaisedButton(
child: Text('Cancel'),
onPressed: () {
Navigator.pop(context);
}
)
)
]
)
],
)
);
}
}
这里用了文本输入框TextField,一个花里胡哨的Widget属性一大堆,和html里的input但是要比input强大很多,想要的效果这个Widget基本上都有,只要加属性就行了。这里用了decoration的prefixIcon输入框前的图标,labelText类似于input的placeholder当输入框获得焦点时虽小到左上角,还有obscureText是否密文。
写到这布局基本上就算完成了,接下来加状态,HomePage加一个登录状态,Login加上username和password,并在点Login的时候做空校验,通过之后跳转回HomePage并带上登录状态。
首先在Login里添加两个state,username和password,并在两个TextField里监听onChanged事件,给username和password赋值,并在点击Login的时候做校验。
TextField
TextField (
decoration: InputDecoration(
prefixIcon: Icon(Icons.person),
labelText: ('username')
),
onChanged: (value) {
setState(() {
username = value;
});
},
)
username和password一样,就不重复了。
Cancel
RaisedButton(
child: Text('Cancel'),
onPressed: () {
setState(() {
username = '';
password = '';
});
Navigator.pop(context);
}
)
取消的时候将username和password置空。
Login
RaisedButton(
child: Text('Login'),
color: Colors.blue,
onPressed: () {
String tip = '';
if (username == '') {
tip = 'username empty';
} else if (password == '') {
tip = 'password empty';
} else {
setState(() {
username = '';
password = '';
});
tip = 'success';
}
print(tip);
}
)
在这里对username和password做校验,得到不同的tip。
三种不同状态,可以打印出来。
SnackBar。
import 'package:flutter/material.dart';
import 'dart:async';
class Login extends StatefulWidget {
@override
_LoginState createState() => new _LoginState();
}
class _LoginState extends State<Login> {
String username = '';
String password = '';
@override
Widget build(BuildContext context) {
return Scaffold(
body: Builder (builder: (BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container (
padding: EdgeInsets.only(
top:50,
left: 20,
right: 20
),
child: TextField (
decoration: InputDecoration(
prefixIcon: Icon(Icons.person),
labelText: ('username')
),
onChanged: (value) {
setState(() {
username = value;
});
},
)
),
Container (
padding: EdgeInsets.all(20),
child: TextField (
decoration: InputDecoration(
prefixIcon: Icon(Icons.lock),
labelText: ('password')
),
obscureText: true,
onChanged: (value) {
setState(() {
password = value;
});
}
)
),
Row (
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container (
margin: EdgeInsets.only(
left: 10,
right: 10
),
child: RaisedButton(
child: Text('Login'),
color: Colors.blue,
onPressed: () {
String tip = '';
if (username == '') {
tip = 'username empty';
} else if (password == '') {
tip = 'password empty';
} else {
setState(() {
username = '';
password = '';
});
Timer(
Duration(seconds: 1),
() {
Navigator.pop(context, true);
}
);
tip = 'success';
}
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(tip),
duration: Duration(seconds: 1),
)
);
}
)
),
Container (
margin: EdgeInsets.only(
left: 10,
right: 10
),
child: RaisedButton(
child: Text('Cancel'),
onPressed: () {
setState(() {
username = '';
password = '';
});
Navigator.pop(context, false);
}
)
)
]
)
],
);
})
);
}
}
这里有几处处改动,Column套了个Builder方法,否则在用Scaffold.of(context)的时候会报错,查了一下是context的问题。另一个是Login的onChanged,这里加了SnackBar,duration是提示的时间,如果登录成功开了个一秒的定时器,在一秒之后返回上一页,这里需要注意的一点就是,Timer是在dart/sync这个包里,需要import进来,另外在登录成功和取消按钮跳转回前一页时传回一个登录状态的参数。
接下来把main.dart稍作改动
import 'package:flutter/material.dart';
import 'package:flutter_demo/login.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => new _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool logged = false;
@override
Widget build(BuildContext context) {
return Scaffold (
appBar: AppBar(
title: Text('Login')
),
body: Center (
child: logged ? Image.asset('./images/logo.png')
:
RaisedButton(
child: Text('Login'),
color: Colors.blue,
onPressed: () async {
var result = await Navigator.push(
context,
MaterialPageRoute(
fullscreenDialog: true,
builder: (context) => Login()
)
);
setState(() {
logged = result;
});
}
)
)
);
}
}
在这同样加一个logged表示登录状态,Center的child根据logged判断显示图片还是按钮,Login按钮的onPressed事件这需要注意一下,加了sync表示这是一个异步方法,Navigator这返回一个Feature,需要await一下,看着和js的async、await、promise用法一样,result就是返回的参数,把他set给logged,就实现了我们的需求。
再来看一下效果
11、网络请求
前边的登录是在app里模拟,但是写应用是离不开网络请求的,接下来我们添加个网络请求,调个登录接口。
这里用一个封装好的第三方网络请求github.com/flutterchin…
还是在pubspec.yaml里添加dio。
dependencies:
dio: 3.0.7
flutter:
sdk: flutter
添加好之后android studio会提示需要下载这个包,或者在命令行
flutter pub get
下载好了之后在login.dart中import进来。
import 'package:dio/dio.dart';
接下来就是改写代码,首先在state里声明一个dio
onpressed中添加网络请求
RaisedButton(
child: Text('Login'),
color: Colors.blue,
onPressed: () async {
String tip = '';
if (username == '') {
tip = 'username empty';
} else if (password == '') {
tip = 'password empty';
} else {
try {
Response res = await dio.post(
'http://118.25.7.84:10086/login', //这个是我自己的服务器提供的接口,可以直接用
data: {
'username': username,
'password': password
}
);
print(res.data);
tip = res.data;
} catch (e) {
print(e);
tip = 'failed';
}
}
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(tip),
duration: Duration(seconds: 1),
)
);
}
)
具体这就实现了网络请求,接口返回登录成功,另外用了try catch捕获异常。
贴一下完整代码
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:dio/dio.dart';
class Login extends StatefulWidget {
@override
_LoginState createState() => new _LoginState();
}
class _LoginState extends State<Login> {
Dio dio = new Dio();
String username = '';
String password = '';
@override
Widget build(BuildContext context) {
return Scaffold(
body: Builder (builder: (BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container (
padding: EdgeInsets.only(
top:50,
left: 20,
right: 20
),
child: TextField (
decoration: InputDecoration(
prefixIcon: Icon(Icons.person),
labelText: ('username')
),
onChanged: (value) {
setState(() {
username = value;
});
},
)
),
Container (
padding: EdgeInsets.all(20),
child: TextField (
decoration: InputDecoration(
prefixIcon: Icon(Icons.lock),
labelText: ('password')
),
obscureText: true,
onChanged: (value) {
setState(() {
password = value;
});
}
)
),
Row (
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container (
margin: EdgeInsets.only(
left: 10,
right: 10
),
child: RaisedButton(
child: Text('Login'),
color: Colors.blue,
onPressed: () async {
String tip = '';
if (username == '') {
tip = 'username empty';
} else if (password == '') {
tip = 'password empty';
} else {
try {
Response res = await dio.post(
'http://118.25.7.84:10086/login',
data: {
'username': username,
'password': password
}
);
print(res.data);
tip = res.data;
Timer(
Duration(seconds: 1),
() {
Navigator.pop(context, true);
}
);
} catch (e) {
print(e);
tip = 'failed';
}
}
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(tip),
duration: Duration(seconds: 1),
)
);
}
)
),
Container (
margin: EdgeInsets.only(
left: 10,
right: 10
),
child: RaisedButton(
child: Text('Cancel'),
onPressed: () {
setState(() {
username = '';
password = '';
});
Navigator.pop(context, false);
}
)
)
]
)
],
);
})
);
}
}
目前flutter已经支持MacOs,可以在吗命令行执行以下命令
flutter config --enable-macos-desktop
在项目目录下执行
flutter create .
即可体验
在android studio的设备下啦列表了会多一个macOs(desktop),选中后就可以跑起来了。
这里需要注意的是,跑起来之后调接口是会失败的,需要在macos/Runner/DebugProfile.entitlements文件中添加com.apple.security.network.client。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
以上代码在http_server分支。
最后还是要吐槽一下flutter的地狱嵌套,当然和我没有拆分组件有关系。。。看着脑袋疼。