Flutter培训提纲

201 阅读18分钟

一、环境配置

1、配置java环境

1、下载java JDK

Oracle官网下载jdk17:www.oracle.com/java/techno…

image.png 登录Oracle账号,若无账号则创建账号并且登录。

image.png

2、安装jdk

双击下载完成的jdk进行安装,记录安装路径。

image.png

  1. 配置环境变量 我的电脑->右键->属性->高级系统设置

image.png

点击 “环境变量”

image.png

修改以下系统变量(非用户变量),若不存在,则新建。

  • 变量名:JAVA_HOME
  • 变量值:电脑上JDK安装的绝对路径

image.png

  • 变量名:CLASSPATH
  • 变量值:.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;

image.png

最前面的.不要漏掉!

  • 变量名:Path
  • 变量值:
  • %JAVA_HOME%\bin
  • %JAVA_HOME%\jre\bin

image.png

3、验证是否配置成功

打开命令行工具:win+R,输入cmd,点击确定。

image.png

输入 java -version ,若正常显示出版本号,则配置成功。

image.png

2、Android环境配置

1、下载Android Studio

进入android studio官网,下载Android Studio:developer.android.google.cn/studio/ 向下拉,选择绿色版,安装较为简单。

image.png

2、安装AS

  1. 下载完成后解压到非系统盘,进入目录:/bin,双击 studio64.exe

image.png

image.png

  1. 取消Android SDK自动安装

image.png

  1. 点击下一步

image.png

  1. 选择Custom点击Next

image.png

  1. 选择UI风格

image.png

  1. 设置Android SDK,若本地无SDK可自己设置一个目录(最好不要设置到系统盘),Android studio会自动下载Android SDK。

image.png

  1. 模拟器设置,直接点下一步

image.png

  1. 点击finish,安装Android SDK

image.png

3、配置环境变量

非必须步骤

  • 变量名:ANDROID_HOME
  • 变量值:Android SDK的安装路径

image.png

  • 变量名:Path
  • 变量值:
  • %ANDROID_HOME%\tools
  • %ANDROID_HOME%\platform-tools

image.png

3、Flutter环境配置

1、下载Flutter SDK

docs.flutter.cn/release/arc…

下载完成后解压到本地磁盘。(例如 D:\flutter\flutter)

image.png

请注意

  • 请勿将 Flutter 有特殊字符或空格的路径下。
  • 勿将 Flutter 安装在需要高权限的文件夹内,例如 C:\Program Files\。

2、设置环境变量

  • 变量名:Path
  • 变量值:flutter\bin 目录的完整路径

image.png

image.png

image.png

3、运行flutter doctor

打开命令行窗口,输入 flutter doctor

image.png 上述命令会检查你的现有环境,并将检测结果以报告形式呈现出来。仔细阅读它显示的内容,检查是否有尚未安装的软件或是有其他的步骤需要完成(通常会以粗体呈现,Visual Studio与Android Studio安装一个即可)。

4、安装Flutter插件

以Android Studio为例:

1、安装dart插件

打开Android Studio,选择Plugins

image.png

搜索 dart,点击 Install 按钮进行安装。

image.png

2、安装flutter插件

搜索 flutter,点击 Install 按钮进行安装。

image.png

安装完成后重启Android studio生效

二、Dart语法

变量声明

int i = 1;
double d = 0.1;
bool b = true;
String str = "";
Object o = 1.1;
//可变数据类型
dynamic dy = 1.1;
//允许
dy = "hello";
//类型推导
var v = 1.1;
//不允许
v = "hello"; 

思考: Object、dynamic、var的区别?

私有变量

Java是通过private关键字声明私有变量或者私有函数的。而Dart没有private/public关键字,默认声明的函数或者变量都为public属性,如果函数或者变量以 _ 开头,则为private属性。不可见属性是相对于文件不可见。

常量

1、const:编译时常量

必须在编译阶段完成赋值,必须为编译常量。

//编译常量,在编译时赋值
const int age = 23;
//时间戳运行时赋值,此语法不允许
const DateTime timeStamp = DateTime.now();

const修饰包含成员变量或者方法的对象时,实例本身以及成员皆不可修改。

const arr = [1];
//此操作不允许,变量实例不允许修改
arr = [1, 2];
//此操作不允许,成员变量不允许修改
arr[0] = 4;
debugPrint(arr.toString());

2、final:运行时常量

允许运行时赋值,可在程序执行到此代码块时进行赋值。

//编译常量,允许在编译时赋值
final DateTime timeStamp = DateTime.now();
debugPrint(timeStamp.toString());

final修饰包含成员变量或者方法的对象时,实例本身不可修改,成员变量允许修改。

final arr = [1];
//此操作不允许,变量实例不允许修改
arr = [1, 2];
//此操作允许,成员变量可以修改
arr[0] = 4;
debugPrint(arr.toString());

枚举

enum Country{
  cn,
  us,
  uk
}

与java不同的是,dart的枚举可以添加内部变量。比如我们需要通过枚举类型国家代码,获取国家名字。可以这么做:

void main() {
  Country c = Country.us;

  debugPrint('c: ${c.name}');
}

enum Country{
  cn('中国'),
  us('美国'),
  uk('英国');

  final String name;
  const Country(this.name);

}

反过来,如果我们需要通过国家名字,获取国家的代码,可以这么做:

void main() {
  String name = "美国";
  Country country =  Country.fromName(name);
  debugPrint("country: $country");
}

enum Country {
  cn('中国'),
  us('美国'),
  uk('英国');

  final String name;

  const Country(this.name);

  static Country fromName(String name) {
    return values.firstWhere(
          (element) => element.name == name,
      orElse: () => cn,
    );
  }
}

操作符 / 运算符

1、非空断言/空安全操作符

String? name;
//name?:空安全操作符
int? l1 = name?.length;
//name!:非空断言操作符
int l2 = name!.length;

2、除法,整除、取模

main() {
  var i = 7;
  debugPrint("${i / 3}"); //除法操作,结果2.3333333333333335
  debugPrint("${i ~/ 3}"); //整除,结果2
  debugPrint("${i % 3}"); //取模,结果1
}

3、??= 赋值操作

  • 当变量为null时,使用后面的内容进行赋值。
  • 当变量不为null时,使用自己原来的值。

就是如果该变量不为null,该运算符操作无效

String? userName = "Tom";
userName ??= "Jerry"; //变量本身有值,使用变量本身的值
debugPrint(userName); //结果为"Tom"

String? nickName;
nickName ??= "猫"; //变量本身为null,使用后面的值
debugPrint(nickName); //结果为猫

4、expr1??expr2条件运算符

  • 如果expr1为null,返回expr2的结果。
  • 如果expr1不为null,返回expr1的结果。
String str1 = "Tom" ?? "Jerry"; //结果为"Tom"
String str2 = null ?? "Jerry";  //结果为"Jerry"

5、级联运算符

..完成链式调用

class Person {
  String? name;
  void run() {
    debugPrint("$name is running");
  }
  void eat() {
    debugPrint("$name is eating");
  }
}
main() {
  Person p1 = Person();
  p1.name = "Tom";
  p1.run();
  p1.eat();

  Person p2 = Person()
    ..name = "Jerry"
    ..run()
    ..eat();
}

6、解构操作符

... 将数组解构为单个的item

List<int> list1 = [1, 2, 3];
List<int> list2 = [0, ...list1, 4];
debugPrint("list2: $list2"); //[0, 1, 2, 3, 4]

7、is 关键字

用于检查某个对象是否属于某个类型,相当于Java的instanceof。与Java不同的是is还有一个否行形式 is! 。

var i = 1;
//相当于Java的 i instanceof Integer
debugPrint("${i is int}");  //true
debugPrint("${i is! int}"); //false

函数

Dart不支持重载,但是支持可选参数。

1、位置可选参数

main() {
  printInfo("Tom");
  printInfo("Tom", null);
  printInfo("Tom", null, 1.98);
  printInfo("Tom", 15, 1.98);
}

/// 位置可选参数(可选参数使用[])
void printInfo(String name, [int? age, double height = 1.88]) {
  debugPrint("$name, $age, $height");
}

2、命名可选参数

main() {
  printMessage("Jerry");
  printMessage("Jerry", age: 17);
  printMessage("Jerry", height: 1.68);
  printMessage("Jerry", age: 17, height: 1.68);
}

///命名可选参数(可选参数使用{})
void printMessage(String name, {int? age, double height = 1.88}) {
  debugPrint("$name, $age, $height");
}

3、函数一等公民

1、函数作为参数:

void main() {
  say(getMessage);
}

void say(String Function() func) {
  String message = func();
  debugPrint("say: $message");
}

String getMessage() {
  return 'Hello World!';
}

2、函数作为返回值:

void main() {
  Function fun = say();
  debugPrint("say: ${fun()}");
}

String Function() say() {
  return getMessage;
}

String getMessage() {
  return 'Hello World!';
}

1、构造函数

class Rectangle {
  int width = 0;
  int height = 0;

  //构造函数
  // Rectangle(int width, int height) {
  //   this.width = width;
  //   this.height = height;
  // }

  //Dart支持的语法糖写法
  Rectangle(this.width, this.height);

}

2、命名构造函数

void main() {
  Rectangle r1 = Rectangle(10, 11);
  Rectangle r2 = Rectangle.fromJson({'width': 100, 'height': 110});
}

class Rectangle {
  int width = 0;
  int height = 0;

  Rectangle(this.width, this.height);

  // 假设这个构造函数用于从一个JSON对象中创建Rectangle对象
  Rectangle.fromJson(Map<String, int> json) {
    width = json['width'] ?? 0;
    height = json['height'] ?? 0;
  }
}

3、初始化列表

void main() {
  Rectangle r1 = Rectangle(10, 11);
}

class Rectangle {
  int width = 0;
  int height = 0;
  int? area;

  Rectangle(this.width, this.height) : area = width * height;

}

集合

List

List<int> list = [3, 4, 3];
//取值
int item = list[0];
//插入
list.add(5);
list.insert(0, 1);
//删除
list.remove(1);
list.removeAt(0);
list.removeRange(0, 1);
list.removeWhere((element) => element > 4);
//转换
List<String> result = list
    .map((e) => e * 2)
    .where((element) => element > 5)
    .map((e) => '年龄为:$e')
    .toList();
//遍历
for (var element in list) {
  debugPrint("$element");
}
for (var i = 0; i < result.length; ++i) {
  var o = result[i];
  debugPrint(o);
}

Map

Map<String, dynamic> map = {
  'age': 1,
};
//取值
int? item = map['age'];
//插入
map['height'] = 1.8;
//删除
map.remove('height');
map.removeWhere((key, value) => value is int);
//转换
Map<String, String> result = map.map((key, value) => MapEntry(key, value.toString()));
//遍历
map.forEach((key, value) {
debugPrint("key:$key,value:$value");
});
for (var key in map.keys) {
debugPrint("key:$key,value:${map[key]}");
}

Set

Set set = {'Tom', 'Jerry'};
//利用Set去重
List<String> list = ['Tom', 'Jerry', 'Jerry'];
list = list.toSet().toList();
debugPrint("list: $list");

接口

dart中无Interface,使用函数作为回调接口使用。

void main() {
  MyButton button = MyButton(
    '按钮',
        () => debugPrint("点击了按钮"),
  );

  button.onPressed();
}

class MyButton {
  String text;
  Function() onPressed;

  MyButton(this.text, this.onPressed);
}

混入

混入是Dart提供的一个多个类中复用相同代码的方式。与继承有所不同。继承只可以单继承,但是复用性强,父类所有变量都会被子类继承。混入可以多混入,但是复用性弱,混入类的变量不属于被混入类。

mixin Jump {
  void jumping() {
    debugPrint("跳跃");
  }
}

mixin Run {
  void running() {
    debugPrint("奔跑");
  }
}

class Person with Jump, Run {

}

扩展函数

使用 extension 关键字,必须设置类型。

void func(String? name) {
  if (name.isNotBlank) {
    debugPrint("name: $name");
  }
}

extension CustomerExtension on String? {

  bool get isNotBlank {
    if (null != this && this!.isNotEmpty) {
      return true;
    }
    return false;
  }
}

三、StatelessWidget

AppBar

AppBar(
  //标题内容
  title: const Text('Title'),
  //是否自动添加返回按钮
  automaticallyImplyLeading: false,
  //左侧按钮
  leading: IconButton(
    onPressed: () {},
    icon: const Icon(Icons.arrow_back_ios),
  ),
  //阴影
  elevation: 4,
  //背景颜色
  backgroundColor: Colors.purple,
  //右侧按钮
  actions: [
    IconButton(onPressed: () {}, icon: const Icon(Icons.add)),
    IconButton(onPressed: () {}, icon: const Icon(Icons.share)),
  ],
);

Scaffold

Scaffold(
  //自动调整body的大小
  resizeToAvoidBottomInset: false,
  //标题栏
  appBar: AppBar(title: const Text('标题')),
  //页面内容
  body: Container( ),
  //左侧抽屉
  drawer: Drawer(),
  //浮动按钮
  floatingActionButton: FloatingActionButton(onPressed: () {  }, child: const Icon(Icons.add)),
)

Container

Container(
  padding: EdgeInsets.symmetric(vertical: 1),
  //内距离
  alignment: Alignment.center,
  //内部控件居中
  width: 40,
  //宽
  height: 30,
  //高
  child: Icon(Icons.star),
  //color: Colors.red,  //背景色,与BoxDecoration互斥,设置了BoxDecoration就不能使用此属性
  decoration: BoxDecoration(
    color: Colors.red,
    //设置形状
    shape: BoxShape.circle,
    //边框
    border: Border.all(width: 1, color: Colors.red),
    // border: Border(bottom: BorderSide(width: 1, color: Colors.red)),//下划线
    //设置圆角
    borderRadius: BorderRadius.all(Radius.circular(25.0)),
    //设置图片
    image: DecorationImage(image: NetworkImage("XXX.jpg"), fit: BoxFit.cover),
    //设置阴影
    boxShadow: [
      BoxShadow(
        blurRadius: 40, //阴影深度(数字越大阴影越淡)
        offset: Offset(20, 20), //阴影X,Y轴偏移距离
        spreadRadius: 10, //阴影与decoration分割线宽度,可用此属性配合borderRadius实现圆角边框效果
        color: Colors.black26, //阴影颜色
      )
    ],
    //渐变颜色
    //RadialGradient圆形渐变效果
    gradient: LinearGradient(
      colors: [Colors.white, Colors.red], //渐变起止色
      begin: Alignment.bottomCenter, //渐变开始方向
      end: Alignment.topCenter, //渐变结束方向
      stops: [0.4, 0.6], //渐变变化范围,(只有比例尺0.4~0.6范围内渐变,其他范围为纯色)
    ),
  ),
);

Text

Text(
  'Hello World',
  style: TextStyle(
    color: Colors.blue,
    fontSize: 24,
  ),
  maxLines: 2,
  overflow: TextOverflow.ellipsis,
  textAlign: TextAlign.start,
)

除了正常显示的文本,还可以显示富文本。

const Text.rich(TextSpan(
  children: const [
    TextSpan(text: 'Hello', style: TextStyle(fontSize: 15, color: Colors.red),),
    TextSpan(text: 'World', style: TextStyle(fontSize: 26, color: Colors.blue),),
  ],
));

如果是多个Text显示同一个样式,可以统一设置style。

DefaultTextStyle(
  style: TextStyle(fontSize: 20, color: Colors.red),
  child: Column(
    children: [
      Text('Hello'),
      Text('World'),
    ],
  ),
);

Colum / Row

Row(
  //MainAxisAlignment.start 从开始方向排列
  //MainAxisAlignment.end 从结束方向排列
  //MainAxisAlignment.center 居中
  //MainAxisAlignment.spaceBetween 空白区域均分排列子元素,并且首尾没有空隙
  //MainAxisAlignment.spaceAround 空白区域均分排列子元素,中间子控件距离相等,首位子控件距离左右边界距离为中间子控件距离的一半
  //MainAxisAlignment.spaceEvenly 空白区域均分排列子元素,各个子控件距离相等
  mainAxisAlignment: MainAxisAlignment.start,
  //TextDirection.ltr 排列方向从左向右(默认)
  //TextDirection.rtl 排列方向从右向左
  textDirection: TextDirection.rtl,
  //MainAxisSize.min相当于主轴方向wrap_content
  //MainAxisSize.max相当于主轴方向match_parent
  mainAxisSize: MainAxisSize.min,
  children: [
    Text("text1"),
    Text("text2"),
  ],
);

Stack/Positioned/Align

SizedBox(
  width: 300,
  height: 300,
  child: Stack(
    fit: StackFit.loose,
    clipBehavior: Clip.hardEdge,
    children: [
      Container(width: 300, height: 300, color: Colors.yellow),
      Align(
        alignment: Alignment.center,
        child: Container(width: 100, height: 100, color: Colors.red),
      ),
      Positioned(
        bottom: 0,
        right: 0,
        child: Container(width: 100, height: 100, color: Colors.green),
      )
    ],
  ),
)

FractionallySizeBox

可以按照比例设置占用父组件的尺寸

Container(
  width: 300,
  height: 300,
  alignment: Alignment.center,
  child: FractionallySizedBox(
    widthFactor: 0.5,    //宽占用父组件的一半
    heightFactor: 0.5,    //高占用父组件的一半
    child: LayoutBuilder(
      builder: (context, constraints) {
        debugPrint("constraint:$constraints");  //约束为 (w=150.0, h=150.0)
        return const Text("hello");
      },
    ),
  ),
)

Expanded

//尺寸扩张为父组件剩余空间的最大值

Row(
  children: [
    Expanded(
      flex: 2,
      child: Container(height: 60, color: Colors.red),
    ),
    Expanded(
      flex: 1,
      child: Container(height: 60, color: Colors.yellow),
    ),
  ],
)

ConstrainedBox

ConstrainedBox(
  constraints: const BoxConstraints(
    maxWidth: 100,
    maxHeight: 100,
  ),
  child: Container( //ConstrainedBox限制了最大尺寸约束,所以Container的尺寸无效。
    width: 200,
    height: 200,
    color: Colors.red,
  ),
)

FittedBox

FittedBox(
  //BoxFit.fill:会拉伸或压缩子部件以填充 FittedBox,这可能会导致子部件的宽高比发生变化。
  //BoxFit.contain:会尽可能大的展示子部件,同时保持子部件的宽高比,所以子部件的一部分可能会在 FittedBox 内部无法显示。
  //BoxFit.cover:会尽可能小的展示子部件,同时保持子部件的宽高比,所以子部件的全部内容都可以在 FittedBox 内部显示,但可能无法完全填满 FittedBox。
  //BoxFit.fitWidth:会拉伸或压缩子部件以填满 FittedBox 的宽度,同时保持子部件的宽高比。
  //BoxFit.fitHeight:会拉伸或压缩子部件以填满 FittedBox 的高度,同时保持子部件的宽高比。
  //BoxFit.none:不会对子部件进行缩放,子部件将会以其原始大小进行渲染。
  //BoxFit.scaleDown:会尽可能小的展示子部件,同时保持子部件的宽高比,但子部件的大小不会超过其原始大小。
  fit: BoxFit.scaleDown,
  child: Text(
    '一二三四一二三四',
    style: TextStyle(
      fontSize: 14,
      color: Colors.white,
    ),
  ),
)

Button

ElevatedButton

ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    //设置背景色
    backgroundColor: Colors.blue,
    //设置边框
    side: const BorderSide(width: 2, color: Colors.red),
    //设置圆角
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
    //设置按钮的大小
    minimumSize: const Size(200, 100),
    //设置按钮的padding
    padding: const EdgeInsets.all(20),
    //设置按钮的elevation效果
    elevation: 10,
    //设置按钮的阴影颜色
    shadowColor: Colors.green,
  ),
  child: const Text('ElevatedButton'),
)

TextButton

TextButton(
  onPressed: () {},
  child: const Text('Button'),
)

OutlinedButton

OutlinedButton(
  onPressed: () {},
  child: const Text('Button'),
)

Scroll

SingleChildScrollView

使超过屏幕高度的子组件可以上下滚动,防止越界。

与ListView有个不同是,不需要滚动时,用户感觉不到他的存在。

SingleChildScrollView(
  child: SizedBox(
    width: 800,
    child: Text("Hello Flutter" * 800),
  ),
)

ListView

1、固定子项(与SingleChildScrollView类似,区别是即便不需要滚动时,拖动时依然可以感受到它的存在)

ListView(
  children: [
    FlutterLogo(),
    FlutterLogo(),
    FlutterLogo(),
  ],
)

2、带懒加载与子项复用

final _controller = ScrollController();

ListView.builder(
  //ClampingScrollPhysics(),  Android默认效果,拖动到边界后固定。
  //BouncingScrollPhysics(),  IOS默认效果,拖动到边界后反弹。
  //NeverScrollableScrollPhysics(),  不可滚动,禁用ListView滚动功能。
  //Android默认效果
  physics: const NeverScrollableScrollPhysics(),
  controller: _controller,
  itemCount: 100,
  cacheExtent: 0,
  //预加载区域大小。
  itemExtent: 50,
  //固定每个item主轴方向的尺寸,设置后item再设置大小无效。
  shrinkWrap: true,
  //真空包装,相当于Android的wrap_content
  padding: const EdgeInsets.all(50),
  //ListView内部的padding
  scrollDirection: Axis.horizontal,
  //设置ListView方向
  itemBuilder: (context, index) {
    return Container(
      color: Colors.primaries[index % Colors.primaries.length],
      child: Text("$index"),
    );
  },
)

带分割线

ListView.separated(
  itemCount: 100,
  itemBuilder: (context, index) {
    return SizedBox(height: 50, child: Text("$index"));
  },
  separatorBuilder: (BuildContext context, int index) {
    //添加分割线
    return const Divider(thickness: 1);
  },
)

GridView

GridView.builder(
  itemCount: 100,
  //固定交叉轴方向最大数量
  // gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount (
  //   crossAxisCount: 3,
  //   childAspectRatio: 16 / 9,
  //   crossAxisSpacing: 10,
  //   mainAxisSpacing: 10,
  // ),
  //固定交叉轴方向最大尺寸
  gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
    maxCrossAxisExtent: 120,  //交叉轴方向尺寸
    childAspectRatio: 16 / 9,
  ),
  itemBuilder: (context, index) {
    return Container(color: Colors.primaries[index % Colors.primaries.length]);
  },
)

image.png

如果固定个数的item,不需要builder的话,可以使用语法糖写法,使用GildView.count或者GridView.extent

//交叉轴固定个数
GridView.count(
  crossAxisCount: 4,
  children: List.generate(
    36,
    (index) => Container(color: Colors.primaries[index % Colors.primaries.length]),
  ),
)

//交叉轴固定尺寸
GridView.extent(
  maxCrossAxisExtent: 100,
  children: List.generate(
    36,
    (index) => Container(color: Colors.primaries[index % Colors.primaries.length]),
  ),
)

四、StatefulWidget

组件渲染与刷新的原理:

Widget 、Element(State) 、RenderObject

TextField:

var controllerPhone = TextEditingController(text: "默认值");

TextField(
    controller: controllerPhone,
    style: const TextStyle(fontSize: 20),
    obscureText: true, //密码输入样式
    textAlign: TextAlign.start, //文字靠左显示
    //输入框多行输入
    maxLines: 1,
    //键盘只能输入数字
    keyboardType: TextInputType.number,
    //TextCapitalization.words:每个单词首字母默认大写字母
    //TextCapitalization.sentences:每句话字母默认大写字母
    //TextCapitalization.characters:每个字符首字母默认大写字母
    textCapitalization: TextCapitalization.none,
    //输入格式
    inputFormatters: [
      FilteringTextInputFormatter.digitsOnly, //只允许输入整数
      // LengthLimitingTextInputFormatter(11), //限制输入字数位11
      // FilteringTextInputFormatter(RegExp("[0-9.]"), allow: true), //允许输入数字包括小数
      // FilteringTextInputFormatter(RegExp("[a-zA-Z]"), allow: true), //允许输入字母
    ],
    decoration: InputDecoration(
      //前端外置icon
      icon: Icon(Icons.person),
      //后缀icon
      suffixIcon: Icon(Icons.person),
      //前端内置icon
      prefixIcon: Icon(Icons.person),
      //label提示
      labelText: "用户名",
      //hint
      hintText: "请输入用户名",
      //正常状态时的border
      enabledBorder: UnderlineInputBorder(borderSide: BorderSide(width: 1, color: Colors.orange)),
      //存在焦点时的border
      focusedBorder: OutlineInputBorder(borderSide: BorderSide(width: 1, color: Colors.black), borderRadius: BorderRadius.circular(30)),
      //底下添加help
      helperText: "我是help",
      //count
      counterText: textController.text.length.toString(),
      //填充颜色
      fillColor: Colors.blue,
      filled: true,
      //去掉输入框上下的间隔
      isCollapsed: true,
      //去边框
      border: InputBorder.none,
    ),
    //输入内容改变监听
    onChanged: (text) {
      debugPrint(text);
    },
);

Key

Key的作用是什么?什么情况下应该使用什么Key?结合Flutter绘制原理与代码演示来讲。

1、LocalKey

只有本层组件树可见,效率较高。

  • ValueKey():比较值,相当于java的 equals
  • ObjectKey():比较对象内存指针是否相同,相当于java的 ==
  • UniqueKey():自动生成一个随机的key

2、GlobalKey

所有组件树均可见,如非必要使用localKey。 组件设置GlobalKey后,可使用GlobalKey获取此组件的Widget、State、BuildContext。

final _globalKey = GlobalKey();
Container(
  key: _globalKey,
)
//通过globalKey获取weidget
_globalKey.currentWidget as XXXWidget; 
//通过globalKey获取state
_globalKey.currentState as XXXState;
//通过globalKey获取BuildContext
_globalKey.currentContext;
//通过BuildContext获取组件尺寸
RenderBox renderBox = _globalKey.currentContext?.findRenderObject() as RenderBox; //普通组件直接转换为renderBox
var height = renderBox.size.height; //获取组件高度

Form

class FormWidgetState extends State<FormWidget> {
  String phone = '';
  GlobalKey<FormState> formGlobalKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: formGlobalKey,
      child: Column(
        children: [
          const SizedBox(height: 30),
          TextFormField(
            autovalidateMode: AutovalidateMode.disabled,
            onSaved: (value) {
              phone = value ?? "";
            },
            validator: (value) {
              RegExp reg = RegExp(r'^\d{11}$');
              return reg.hasMatch(value!) ? null : '请输入11位手机号码';
            },
          ),
          ElevatedButton(
            onPressed: () {
              var _currentForm = formGlobalKey.currentState;
              if (_currentForm!.validate()) {
                print("验证通过");
              }
            },
            child: Text("按钮"),
          ),
        ],
      ),
    );
  }
}

五、路由

1、匿名路由

  • 跳转新的路由
//跳转
Navigator.of(context).push(MaterialPageRoute(
  builder: (context) => const PageTow(),
));


//传参、接收回传
dynamic result = await Navigator.of(context).push(MaterialPageRoute(
  builder: (context) => const PageTwo(name: 'Tom'),
));

//页面B接收参数、回传参数
class PageTwo extends StatelessWidget {
  final String? name;

  const PageTwo({super.key, this.name});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ElevatedButton(
        onPressed: () {
          //关闭页面并回传
          Navigator.of(context).pop('jerry');
        },
        child: const Text('返回'),
      ),
    );
  }
}
  • 跳转新的路由,并关闭当前路由
Navigator.of(context).pushReplacement(MaterialPageRoute(
  builder: (context) => const PageTow(),
));
  • 关闭当前路由
Navigator.of(context).pop();

2、命名路由

  • MaterialApp设置路由列表与初始化路由
MaterialApp(
  title: 'Flutter Demo',
  initialRoute: '/',
  routes: {
    '/': (context) => const PageOne(),
    '/pageTwo': (context) => const PageTwo(),
  },
);
  • 跳转新的路由
//跳转新的路由
Navigator.of(context).pushNamed('/pageTwo');

//跳转并传值、接收回传
dynamic name = await Navigator.of(context).pushNamed('/pageTwo', arguments: 'Tom');
debugPrint("name: $name");

//页面2接收参数
dynamic name = ModalRoute.of(context)?.settings.arguments;
debugPrint("name: $name");

//页面2关闭页面并回传
Navigator.of(context).pop('jerry');

  • 跳转新的路由,并关闭当前路由
//跳转
Navigator.of(context).pushReplacementNamed('/pageTwo');
  • 关闭当前路由
Navigator.of(context).pop();

六、异步

Flutter异步原理:

EventLooper机制、EventQueue

Future

async/await语法糖

Future<String> futureFun() {
  // return Future(() => "future");
  return Future.error(Exception("出现错误了"));
}


ElevatedButton(
  onPressed: () {
    futureFun()
        .then((value) => debugPrint(value)) //Futrue执行完成后调用
        .catchError((exception) => debugPrint(exception.toString())) //Futrue异常后调用
        .whenComplete(() => debugPrint("无论如何我都会执行"));
  },
  child: Text('test'),
);

为了防止回调地狱,一般使用 async 与 await 配合来使用。

Future<String> getUserId() async {
  return Future.error("抛出异常");
  // return Future(() => "123");
}


ElevatedButton(
  onPressed: () async {
      try {
        String userId = await getUserId();
        debugPrint(userId);
      } catch (error) {
        debugPrint(error.toString());
      }
    },
  child: Text('test'),
);

FutrueBuilder

Center(
  child: FutureBuilder(
    future: Future.delayed(const Duration(seconds: 1), () => "content"),
    // future: Future.delayed(const Duration(seconds: 1), () => throw("出错了")),
    builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
      if (snapshot.connectionState == ConnectionState.waiting) {
        return const CircularProgressIndicator();
      } else if (snapshot.connectionState == ConnectionState.done) {
        // if (null != snapshot.data) {
        //    return Text(snapshot.data);
        // } else {
        //   return Text("${snapshot.error!}");
        // }
        if (snapshot.hasError) {
          return const Icon(Icons.error);
        }
        if (snapshot.hasData) {
          return Text(snapshot.data);
        }
      }
      throw "went error";
    },
  ),
);

因为 snapshot.data 与 snapshot.error 中一定有一个不为空,可以简写一下。

Center(
  child: FutureBuilder(
    // future: Future.delayed(const Duration(seconds: 2), () => "content"),
    future: Future.delayed(const Duration(seconds: 1), () => throw("出错了")),
    //在Futrue没有完成之前,设置了data的值,这样在Futrue加载过程中,snapshot.hasData一定为ture
    initialData: "initData",
    builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
      if (snapshot.hasError) {
        return const Icon(Icons.error);
      }
      if(snapshot.hasData) {
        return Text(snapshot.data);
      }
      return const CircularProgressIndicator();
    },
  ),
)

Stream

final future = Future.delayed(const Duration(seconds: 1), () => 42);
final stream = Stream.periodic(const Duration(seconds: 1), (count) => 12);

@override
void initState() {
  super.initState();
  //Future使用then监听,延时1秒后打印出出42
  future.then((value) => debugPrint("$value"));
  //Stream使用Liten监听,每隔一秒打印一次12
  stream.listen((event) => debugPrint("event:$event"));
  //Stream执行4次以后停止
  stream.take(4);
  stream.takeWhile((element) => element < 4)
}

StreamBuilder

final stream = Stream.periodic(const Duration(seconds: 1), (count) => count);

StreamBuilder(
  stream: stream,
  builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
    switch (snapshot.connectionState) {
      case ConnectionState.none:
        return const Text("未添加数据流");
      case ConnectionState.waiting:
        return const Text("等待数据流");
      case ConnectionState.active:
        if (snapshot.hasData) {
          return Text("Stream content:${snapshot.data}");
        } else {
          return Text("Stream 出错:${snapshot.error}");
        }
      case ConnectionState.done:
        return const Text("Stream 已经关闭");
    }
  },
)

Stream监听

  • 默认一个Stream只能存在一个监听者。StreamBuilder与listen不能同时存在,两个Listen也不能同时存在。
  • 若需要两个同时监听,可以使用.broacast方式创建,但是此方式创建,不会缓存数据。
//正常创建Stream的方式,所有点击按钮发送的事件,5秒延时过后都会被监听到。
final controller = StreamController();
//广播方式创建的Stream,点击事件不会被缓存,5秒延时过后,才开始监听。
final controller = StreamController.broadcast();

@override
void dispose() {
  super.dispose();
  controller.close();
}

@override
void initState() {
  super.initState();
  //延时5秒钟后监听Stream
  Future.delayed(const Duration(seconds: 5), () {
    controller.stream.listen(
          (event) => debugPrint("$event"),
      onError: (error) => debugPrint("$error"),
      onDone: () => debugPrint("done"),
    );
  });
}

@override
Widget build(BuildContext context) {
  return Column(
      children: [
        ElevatedButton(onPressed: () => controller.sink.add(10), child: const Text("10")),
        ElevatedButton(onPressed: () => controller.sink.addError("出错了"), child: const Text("报错了")),
        ElevatedButton(onPressed: () => controller.sink.close(), child: const Text("关闭")),
      ]
  );
}

七、动画

隐式动画

Animated...

AnimatedContainer

Container的属性变化时(比如尺寸,颜色,圆角等,仅限于自己的尺寸,对子控件无效)产生动画

AnimatedContainer(
  duration: const Duration(milliseconds: 1000),
  width: 300,
  height: 300,
  //动画曲线为完成回弹,默认为线性的Cureves.linear
  curve: Curves.bounceOut,
  decoration: BoxDecoration(
    //渐变色
    gradient: const LinearGradient(
      begin: Alignment.topCenter,
      end: Alignment.bottomCenter,
      colors: [Colors.white, Colors.red],
      stops: [0.3, 0.4],  //渐变区域
    ),
    //加边框,
    boxShadow: const [
      BoxShadow(
        spreadRadius: 20, //加边框
        blurRadius: 20, //加阴影
      )
    ],
    //加圆角
    borderRadius: BorderRadius.circular(150),
  ),
)

AnimatedSwitcher

两个组件之间动画切换效果,如果两个组件是相同类型的组件,一定要添加key。

AnimatedSwitcher(
  duration: const Duration(milliseconds: 800),
  child: isIcon ? Text("你好", key: UniqueKey()) : Text("Hello", key: UniqueKey()),
)

系统默认添加了FadeTransition,渐变动画

AnimatedSwitcher(
  //系统默认添加了此builder
  transitionBuilder: (child, anim) {
    return FadeTransition(
      opacity: anim,
      child: child,
    );
  },
  //同样可以设置为旋转动画
  transitionBuilder: (child, anim) {
    return RotationTransition(
      turns: anim,
      child: child,
    );
  },
  //缩放动画
  transitionBuilder: (child, anim) {
    return ScaleTransition(
      scale: anim,
      child: child,
    );
  },
  //同样支持嵌套,比如既想要旋转,又想要缩放
  transitionBuilder: (child, anim) {
    return RotationTransition(
      turns: anim,
      child: ScaleTransition(
        scale: anim,
        child: child,
      ),
    );
  },

  duration: const Duration(milliseconds: 800),
  child: isIcon ? Text("你好", key: UniqueKey(), style: const TextStyle(fontSize: 20)) : Text("Hello", key: UniqueKey(), style: const TextStyle(fontSize: 20)),
)

AnimatedOpacity

AnimatedOpacity(
  duration: const Duration(milliseconds: 800),
  opacity: 0.3,
  child: Container(
    width: 300,
    height: 300,
    color: Colors.blue,
  ),
)

AnimatedPadding

AnimatedPadding(
  duration: const Duration(milliseconds: 800),
  padding: EdgeInsets.all(60),
  child: Container(
    width: 300,
    height: 300,
    color: Colors.blue,
  ),
)

AnimatedPositioned

Stack(
  children: [
    AnimatedPositioned(
      top: 0,
      curve: Curves.linear,
      duration: const Duration(milliseconds: 800),
      child: Container(width: 300, height: 300, color: Colors.red),
    ),
  ],
)

TweenAnimationBuilder(自定义)

如果系统未提供默认的AnimatedXXX组件,可以使用TweenAnimation来实现,也可以实现内部组件的动画。

随意搭配其他组件

TweenAnimationBuilder(
  duration: const Duration(seconds: 1),
  tween: Tween(begin: 0.0, end: 1.0),
  builder: (context, value, child) {
    return Opacity(
      opacity: value,
      child: Container(
        width: 300,
        height: 300,
        color: Colors.red,
        child: Text("Hello", style: TextStyle(fontSize: value * 50)),
      ),
    );
  },
)

与Transform.scale配合

TweenAnimationBuilder(
  duration: const Duration(seconds: 1),
  tween: Tween(begin: 0.5, end: 2.5),
  builder: (context, value, child) {
    return Transform.scale(
      scale: value, //1.0为原始大小
      child: const Center(child: Text("Hello", style: TextStyle(fontSize: 50))),
    );
  },
)

与Transform.rotate配合

TweenAnimationBuilder(
  duration: const Duration(seconds: 1),
  tween: Tween(begin: 0.0, end: 6.28),
  builder: (context, value, child) {
    return Transform.rotate(
      angle: value, //0~6.28为一圈
      child: const Center(child: Text("Hello", style: TextStyle(fontSize: 50))),
    );
  },
)

与Transform.translate配合

TweenAnimationBuilder(
  duration: const Duration(seconds: 1),
  tween: Tween(begin: 0.0, end: 1),
  builder: (context, value, child) {
    return Transform.translate(
      offset: Offset(value * 200, value * 200),
      child: const Text("Hello", style: TextStyle(fontSize: 50)),
    );
  },
)

Flutter出于性能考虑,只要移动出父组件的范围后,就不可接收点击事件。任何父组件(包括Positioned)。所以在TweenAnimationBuilder外单独嵌套父组件而不设置大小的话,一旦执行位移动作,很可能子组件接受不到点击事件。

显示动画

...Transition

RotationTransition

class _TestFulState extends State<TestFul> with SingleTickerProviderStateMixin {
  late AnimationController controller;

  @override
  void initState() {
    controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
      lowerBound: 3,  //从第三圈
      upperBound: 5,  //转到第五圈
    );
    controller.addListener(() {
      debugPrint("${controller.value}");
    });
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
    controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("App Bar")),
      body: RotationTransition(
        turns: controller,
        child: Center(child: Container(width: 300, height: 300, color: Colors.blue)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          controller.forward(); //运行一次
          // controller.repeat();  //一直运行
          // controller.repeat(reverse: true); //动画播放后反转播放
          // controller.revise(); //动画倒序循环
          // controller.reset(); //停止,回到初始状态
          // controller.stop(); //停止,原地停止
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

FadeTransition

late AnimationController controller;

@override
void initState() {
  controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
  controller.addListener(() {
    debugPrint("${controller.value}");
  });
  super.initState();
}

@override
void dispose() {
  super.dispose();
  controller.dispose();
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: const Text("App Bar")),
    body: FadeTransition(
      opacity: controller,
      child: Center(child: Container(width: 300, height: 300, color: Colors.blue)),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: () {
        controller.forward(from: 0.5);
      },
      child: const Icon(Icons.add),
    ),
  );
}

ScaleTransition

ScaleTransition(
  scale: controller,
  child: Center(child: Container(width: 300, height: 300, color: Colors.blue)),
)

SlideTransition

SlideTransition(
  //平移offset的x,y参数不是具体的坐标,是自身的倍数
  position: controller.drive(Tween(begin: Offset(0, 0), end: Offset(0, 1))),
  child: Container(width: 100, height: 100, color: Colors.blue),
)

chain

SlideTransition(
  position: Tween(begin: const Offset(0, 0), end: const Offset(0, 1))
      .chain(CurveTween(curve: Curves.bounceOut))//添加一个回弹效果的Tween
      .chain(CurveTween(curve: const Interval(0.5, 1))) //等待时间过去一半后再执行动画
      .animate(controller),
  child: Container(width: 100, height: 100, color: Colors.blue),
)

AnimatedBuilder(自定义)

AnimatedBuilder(
  animation: controller, //不可以自定义Tween,固定的0-1之间切换
  builder: (BuildContext context, Widget? child) {
    return Center(
      child: Container(//动画改变Contariner大小
        width: 300 + controller.value * 100,
        height: 300,
        color: Colors.blue,
        child: const Center(child: Text("Hello", style: TextStyle(fontSize: 50))),//Text不需要任何动画
      ),
    );
  },
)

性能优化原理讲解:

AnimatedBuilder(
  animation: controller,
  builder: (BuildContext context, Widget? child) {
    return Center(
      child: Container(
        width: 300 + controller.value * 100,
        height: 300,
        color: Colors.blue,
        child: child,
      ),
    );
  },
  child: const Center(child: Text("Hello", style: TextStyle(fontSize: 50))),
)

Painter(估计没时间讲Painter了吧?)

八、GetX

路由

1、GetX路由实现原理

Getx路由与原生路由的异同、GetX路由使用时需要注意的问题...

2、匿名路由

//原生跳转
Navigator.of(context).push(MaterialPageRoute(builder: (context){
  return PageTwo();
}));
//GetX跳转
Get.to(PageTwo());

3、命名路由

static const MAIN = "/main"; 
static const PAGE_SECOND = "/page_second";


runApp(GetMaterialApp(
  theme: ThemeData(
    primarySwatch: Colors.red, //设置主题颜色
    // colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),  //不要设置,否则primarySwatch无效
    // useMaterial3: true,  //不要设置,否则primarySwatch无效
  ),
  initialRoute: Routes.MAIN, //默认首页路由
  getPages: [
    GetPage(name: MAIN, page: ()=> MyApp()),
    GetPage(name: PAGE_SECOND, page: ()=> PageSecond()),
  ],   //路由列表
  defaultTransition: Transition.native, //默认路由跳转动画
));

// 打开新的路由
Get.toNamed(Routes.NAME)
// 开新的路由并关闭当前路由
Get.offNamed(Routes.NAME)
// 开新的路由并取消其他所有路由
Get.offAllNamed(Routes.NAME)

状态管理

GetBuilder

1、使用

GetBuilder<ThirdController>(
  init: _ctrl, //init可以不传,系统自动识别导入
  builder: (ctrl) {
    return Center(child: Text("你点击了${_ctrl.count}下按钮", style: const TextStyle(fontSize: 20)));
  },
)

刷新

void increase() {
  count++;
  update();
}

2、多个GetBuilder

可通过设置id的方式,单独刷新。

Column(
  children: [
    GetBuilder<ThirdController>(
      builder: (ctrl) {
        return Text("你点击了${ctrl.count}下按钮");
      },
    ),
    GetBuilder<ThirdController>(
      id: 2,    //设置id单独刷新
      builder: (ctrl) {
        return Text("您点击了${ctrl.count}下按钮");
      },
    ),
  ],
)

刷新

void increase() {
  count++;
  update([2]);//只刷新id为2的GetBuilder
}

3、实现原理

.......

obx

1、使用

1、定义Obs变量

///第一种方式
// var name = Rx<String>("");
// var count = Rx<int>(0);
// var height = Rx<double>(1.74);
// var itemList = Rx<List<String>>([]);
// var itemMap = Rx(Map<String, dynamic>)();

///第二种方式
// var name = RxString("");
// var count = RxInt(0);
// var height = RxDouble(1.74);
// var itemList = RxList<String>([]);
// var itemMap = RxMap<String, dynamic>();

// ///推荐方式(.obs扩展函数)
// var name = "".obs;
// var count = 0.obs;
// var height = 1.74.obs;
// var itemList = <String>[].obs;
// var itemMap = <String, dynamic>{}.obs;

// //自定义类
// var entity = XXXEntity().obs;

  RxInt count = 0.obs;
  var userEntity = UserEntity(123, "李雷").obs;

2、使用Obx监听变量的值。

Obx(() {
  return Column(
    children: [
      Text("您点击了${count.value}下按钮", style: const TextStyle(fontSize: 23)),
      Text("用户的名字是:${userEntity.value.name}", style: const TextStyle(fontSize: 23)),
    ],
  );
})

3、改变变量的值。

//参考源码可以看到,RxInt重载了 + 和 - 运算符,自己取出了value进行了计算。
count.value++;
//更新对象中的值时,需要使用update。
userEntity.update((val) {
  if(val?.name == "韩梅梅") {
    val?.name = "李雷";
  } else {
    val?.name = "韩梅梅";
  }
});
//或者使用refresh()
userEntity.name = "李雷";
userEntity.refresh

2、刷新原理

......

3、使用场景

//Useful for simple local states, like toggles, visibility, themes,

国际化

1、使用步骤

1、GetMaterialApp中配置国际化

GetMaterialApp(
  title: 'Flutter Demo',
  translations: Locales(), //国际化配置文件
  locale: const Locale('zh', 'CN'), //设置默认语音,不设置为系统当前语言
  fallbackLocale:  const Locale('zh', 'CN'), //配置错误的情况下使用的语言
);

2、定义语言配置文件,以英语汉语为例。

final Map<String, String> enUS = {
  "home.set_language": "Set Language",
  "home.chinese": "Chinese",
  "home.english": "English",
};




final Map<String, String> zhCn = {
  "home.set_language": "设置语言",
  "home.chinese": "中文",
  "home.english": "英文",
};



class Locales extends Translations {
  @override
  Map<String, Map<String, String>> get keys => {
    "zh_cn": zhCn,
    "en_US": enUS,
  };

}

3、切换语言

Get.updateLocale(const Locale('zh', 'CN')); 
Get.updateLocale(const Locale('en', 'US'));

2、实现原理

.........

九、作业

0、基础配置

1、接口BaseUrl:apifoxmock.com/m1/2234660-…

2、应用主题色:#FF018786。

1、登录

1、用例

  • 用户名或者密码未填写时,点击登录按钮弹出Toast提示。
  • 用户名密码全部填入,点击按钮,弹出全屏进度条。登录成功跳转前关闭进度条。
  • 登录成功后,用户信息保存到本地或者传递到后面的页面。

2、预览图

image.png

3、接口

说明:用户名为:user_01,密码为:123456。

url:/login

请求方式:post

body参数:

{
    "username": "user_01",
    "password": "123456"
}

正常响应结果

{
    "error": 0,
    "data": {
        "id": 1,
        "token": "f89ssd9f8sdf98sdfsd9fs",
        "username": "user_01",
        "password": "123456",
        "nickname": "太空先锋",
        "imageUrl": "https://img95.699pic.com/photo/40034/7177.jpg_wh860.jpg",
        "gender": 0,
        "birthday": "2000-01-01",
        "email": "space@163.com",
        "phone": "18566469874",
        "address": [
            "江苏",
            "苏州"
        ]
    },
    "message": "登录成功"
}

2、文章列表

1、用例

  • 头像为圆角图片。
  • 点击右上角我的跳转到“我的”页面。
  • 具体的文章内容限制最多显示2行,超出部分显示"..."。
  • 预览图最多显示6个,超过的不显示。
  • 点击列表item跳转到详情。

2、预览图

image.png

3、接口

说明:userId参数为1时可以正确获取数据: apifoxmock.com/m1/2234660-…

url:/list

请求方式:get

parameter参数:userId = 1

正常响应结果

{
    "error": 0,
    "data": [
        {
            "id": 1,
            "userId": 1,
            "userNickname": "太空先锋",
            "userImageUrl": "https://img95.699pic.com/photo/40034/7177.jpg_wh860.jpg",
            "timestamp": "1980-05-10 04:32:59",
            "text": "宇宙航行不是一个人或某群人的事,这是人类在其发展中合乎规律的历史进程。",
            "images": [
                "http://n.sinaimg.cn/spider2021617/84/w1014h670/20210617/687b-krpikqf6811833.jpg",
                "https://photo.tuchong.com/6571731/f/1154097679.jpg",
                "https://img.xianjichina.com/editer/20201230/image/17d28d1d5958143ef43c1648b1a4c24a.jpg",
                "http://5b0988e595225.cdn.sohucs.com/images/20200427/1588f791e0b54a0182ac0103990d2469.jpeg",
                "https://www.kepuchina.cn/more/202010/W020201005592055625133.jpg",
                "https://pics6.baidu.com/feed/37d3d539b6003af3e95fc85b2c2389561038b633.png@f_auto?token=40225699bd577ae408613509f2239ceb",
                "https://img.guanhai.com.cn/a/10001/202205/845de20e3ffeb3d6ddd80051f4111dc6.jpeg",
                "https://d.ifengimg.com/w951_h521_q90_webp/x0.ifengimg.com/res/2021/884EAC0AC507E34A52A88B0E358AA80BD4DFAFBB_size90_w951_h521.jpeg",
                "http://www.beidou.gov.cn/zy/bdsp/202006/W020200623412798276654.png"
            ],
            "readings": 8,
            "likes": 16
        },
        {
            "id": 2,
            "userId": 1,
            "userNickname": "星辰大海",
            "userImageUrl": "https://img.51miz.com/preview/element/00/01/16/49/E-1164929-803B938E.jpg",
            "timestamp": "2020-08-11 01:09:59",
            "text": "我们的征途是星辰大海 !🚀🚀🚀我们的征途是星辰大海 !🚀🚀🚀我们的征途是星辰大海 !🚀🚀🚀我们的征途是星辰大海 !🚀🚀🚀我们的征途是星辰大海 !🚀🚀🚀我们的征途是星辰大海 !🚀🚀🚀",
            "images": [
                "https://img.xianjichina.com/editer/20201230/image/17d28d1d5958143ef43c1648b1a4c24a.jpg",
                "http://5b0988e595225.cdn.sohucs.com/images/20200427/1588f791e0b54a0182ac0103990d2469.jpeg",
                "https://www.kepuchina.cn/more/202010/W020201005592055625133.jpg",
                "https://pics6.baidu.com/feed/37d3d539b6003af3e95fc85b2c2389561038b633.png@f_auto?token=40225699bd577ae408613509f2239ceb",
                "https://img.guanhai.com.cn/a/10001/202205/845de20e3ffeb3d6ddd80051f4111dc6.jpeg",
                "https://d.ifengimg.com/w951_h521_q90_webp/x0.ifengimg.com/res/2021/884EAC0AC507E34A52A88B0E358AA80BD4DFAFBB_size90_w951_h521.jpeg",
                "http://www.beidou.gov.cn/zy/bdsp/202006/W020200623412798276654.png"
            ],
            "readings": 69,
            "likes": 33
        },
        {
            "id": 3,
            "userId": 1,
            "userNickname": "超越",
            "userImageUrl": "https://img.zcool.cn/community/01fb095f603d0511013e31874833b6.jpg@1280w_1l_2o_100sh.jpg",
            "timestamp": "1980-05-10 04:32:59",
            "text": "宇航是国家综合国力的重要体现,对国家的长远发展和民族未来具有深远影响。",
            "images": [
                "https://pics6.baidu.com/feed/37d3d539b6003af3e95fc85b2c2389561038b633.png@f_auto?token=40225699bd577ae408613509f2239ceb",
                "https://img.guanhai.com.cn/a/10001/202205/845de20e3ffeb3d6ddd80051f4111dc6.jpeg",
                "https://d.ifengimg.com/w951_h521_q90_webp/x0.ifengimg.com/res/2021/884EAC0AC507E34A52A88B0E358AA80BD4DFAFBB_size90_w951_h521.jpeg",
                "http://www.beidou.gov.cn/zy/bdsp/202006/W020200623412798276654.png"
            ],
            "readings": 8,
            "likes": 16
        },
        {
            "id": 4,
            "userId": 1,
            "userNickname": "我爱地球",
            "userImageUrl": "https://www.keaitupian.cn/cjpic/frombd/0/253/811481000/1296899053.jpg",
            "timestamp": "1980-05-10 04:32:59",
            "text": "宇宙航行对增强民族自豪感和凝聚力有着重要作用,每一次成功的宇航任务都会激发国民的自豪感和信心。",
            "images": [
                "http://n.sinaimg.cn/spider2021617/84/w1014h670/20210617/687b-krpikqf6811833.jpg",
                "https://photo.tuchong.com/6571731/f/1154097679.jpg",
                "https://img.xianjichina.com/editer/20201230/image/17d28d1d5958143ef43c1648b1a4c24a.jpg",
                "http://5b0988e595225.cdn.sohucs.com/images/20200427/1588f791e0b54a0182ac0103990d2469.jpeg",
                "https://www.kepuchina.cn/more/202010/W020201005592055625133.jpg",
                "https://pics6.baidu.com/feed/37d3d539b6003af3e95fc85b2c2389561038b633.png@f_auto?token=40225699bd577ae408613509f2239ceb"
            ],
            "readings": 8,
            "likes": 16
        },
        {
            "id": 5,
            "userId": 1,
            "userNickname": "勇往直前",
            "userImageUrl": "https://img95.699pic.com/photo/40007/5788.jpg_wh860.jpg",
            "timestamp": "1980-05-10 04:32:59",
            "text": "宇指天际,航指航行,也就是航行于天际,寓意一飞冲天。宇航也指宇宙航行。在大气层外太阳系内的空间航行叫做航天。",
            "images": [
                "http://www.beidou.gov.cn/zy/bdsp/202006/W020200623412798276654.png"
            ],
            "readings": 8,
            "likes": 16
        },
        {
            "id": 6,
            "userId": 1,
            "userNickname": "新年快乐",
            "userImageUrl": "https://bpic.588ku.com/element_origin_min_pic/19/03/07/4b2699b6b1461e1f3290e74af5f5e26b.jpg",
            "timestamp": "1980-05-10 04:32:59",
            "text": "宇航名字的含义可以根据不同的解读方式而有不同的意义。宇航一词可以理解为宇宙航行或航天事业,它代表着对宇宙和太空的探索精神和科学探索的冒险精神。",
            "images": [
                "http://n.sinaimg.cn/spider2021617/84/w1014h670/20210617/687b-krpikqf6811833.jpg",
                "https://photo.tuchong.com/6571731/f/1154097679.jpg",
                "https://img.xianjichina.com/editer/20201230/image/17d28d1d5958143ef43c1648b1a4c24a.jpg",
                "http://5b0988e595225.cdn.sohucs.com/images/20200427/1588f791e0b54a0182ac0103990d2469.jpeg",
                "https://www.kepuchina.cn/more/202010/W020201005592055625133.jpg",
                "https://pics6.baidu.com/feed/37d3d539b6003af3e95fc85b2c2389561038b633.png@f_auto?token=40225699bd577ae408613509f2239ceb",
                "https://img.guanhai.com.cn/a/10001/202205/845de20e3ffeb3d6ddd80051f4111dc6.jpeg",
                "https://d.ifengimg.com/w951_h521_q90_webp/x0.ifengimg.com/res/2021/884EAC0AC507E34A52A88B0E358AA80BD4DFAFBB_size90_w951_h521.jpeg",
                "http://www.beidou.gov.cn/zy/bdsp/202006/W020200623412798276654.png"
            ],
            "readings": 8,
            "likes": 16
        },
        {
            "id": 7,
            "userNickname": "太空先锋",
            "userImageUrl": "https://img95.699pic.com/photo/40034/7177.jpg_wh860.jpg",
            "timestamp": "1980-05-10 04:32:59",
            "text": "宇指天际,航指航行,也就是航行于天际,寓意一飞冲天。作为亚洲第一航天强国、航天大国,中国宇航工业的发展不仅有利于自身的综合国力的不断提升,更有利于世界天缘政治格局的进一步优化。",
            "images": [
                "http://n.sinaimg.cn/spider2021617/84/w1014h670/20210617/687b-krpikqf6811833.jpg",
                "https://img.xianjichina.com/editer/20201230/image/17d28d1d5958143ef43c1648b1a4c24a.jpg",
                "http://5b0988e595225.cdn.sohucs.com/images/20200427/1588f791e0b54a0182ac0103990d2469.jpeg",
                "https://pics6.baidu.com/feed/37d3d539b6003af3e95fc85b2c2389561038b633.png@f_auto?token=40225699bd577ae408613509f2239ceb",
                "https://img.guanhai.com.cn/a/10001/202205/845de20e3ffeb3d6ddd80051f4111dc6.jpeg",
                "http://www.beidou.gov.cn/zy/bdsp/202006/W020200623412798276654.png"
            ],
            "readings": 8,
            "likes": 16
        },
        {
            "id": 8,
            "userId": 1,
            "userNickname": "我爱地球",
            "userImageUrl": "https://www.keaitupian.cn/cjpic/frombd/0/253/811481000/1296899053.jpg",
            "timestamp": "1980-05-10 04:32:59",
            "text": "宇宙航行不是一个人或某群人的事,这是人类在其发展中合乎规律的历史进程。",
            "images": [
                "http://n.sinaimg.cn/spider2021617/84/w1014h670/20210617/687b-krpikqf6811833.jpg",
                "https://photo.tuchong.com/6571731/f/1154097679.jpg",
                "https://img.xianjichina.com/editer/20201230/image/17d28d1d5958143ef43c1648b1a4c24a.jpg",
                "http://5b0988e595225.cdn.sohucs.com/images/20200427/1588f791e0b54a0182ac0103990d2469.jpeg",
                "https://pics6.baidu.com/feed/37d3d539b6003af3e95fc85b2c2389561038b633.png@f_auto?token=40225699bd577ae408613509f2239ceb",
                "https://img.guanhai.com.cn/a/10001/202205/845de20e3ffeb3d6ddd80051f4111dc6.jpeg",
                "https://d.ifengimg.com/w951_h521_q90_webp/x0.ifengimg.com/res/2021/884EAC0AC507E34A52A88B0E358AA80BD4DFAFBB_size90_w951_h521.jpeg",
                "http://www.beidou.gov.cn/zy/bdsp/202006/W020200623412798276654.png"
            ],
            "readings": 8,
            "likes": 16
        },
        {
            "id": 9,
            "userId": 1,
            "userNickname": "我要飞得更高",
            "userImageUrl": "https://bpic.588ku.com/element_water_img/19/10/13/ca8d4ddfd0fcb7d7231e80fc73ef3823.jpg",
            "timestamp": "1980-05-10 04:32:59",
            "text": "那么,载人航天到底意义何在呢?世界载人航天的发展 上世纪,世界经历了太长时间炮火的洗礼,人们都渴望能够进入一个正常发展的时期。",
            "images": [
                "https://img.xianjichina.com/editer/20201230/image/17d28d1d5958143ef43c1648b1a4c24a.jpg",
                "https://d.ifengimg.com/w951_h521_q90_webp/x0.ifengimg.com/res/2021/884EAC0AC507E34A52A88B0E358AA80BD4DFAFBB_size90_w951_h521.jpeg"
            ],
            "readings": 8,
            "likes": 16
        },
        {
            "id": 10,
            "userId": 1,
            "userNickname": "全球⽂明",
            "userImageUrl": "https://www.manpingou.com/uploads/allimg/170223/28-1F223105925311.jpg",
            "timestamp": "1980-05-10 04:32:59",
            "text": "载⼈航天的意义和价值 历史上,郑和下西洋到远洋航海技术成熟,推动了世界贸易的发展、世界市场的开辟和近代科学的⼀系列成就,开始了⼀个全球⽂明的时代。",
            "images": [
                "https://img.xianjichina.com/editer/20201230/image/17d28d1d5958143ef43c1648b1a4c24a.jpg",
                "http://5b0988e595225.cdn.sohucs.com/images/20200427/1588f791e0b54a0182ac0103990d2469.jpeg",
                "https://www.kepuchina.cn/more/202010/W020201005592055625133.jpg",
                "https://pics6.baidu.com/feed/37d3d539b6003af3e95fc85b2c2389561038b633.png@f_auto?token=40225699bd577ae408613509f2239ceb",
                "https://img.guanhai.com.cn/a/10001/202205/845de20e3ffeb3d6ddd80051f4111dc6.jpeg",
                "https://d.ifengimg.com/w951_h521_q90_webp/x0.ifengimg.com/res/2021/884EAC0AC507E34A52A88B0E358AA80BD4DFAFBB_size90_w951_h521.jpeg",
                "http://www.beidou.gov.cn/zy/bdsp/202006/W020200623412798276654.png"
            ],
            "readings": 8,
            "likes": 16
        }
    ],
    "message": "成功"
}

3、我的

1、用例

  • 顶部个人信息从本地读取或者从登录页面获取。
  • 点击我的文章列表,跳转到详情页。
  • 具体的文章内容限制最多显示2行,超出部分显示"..."。

2、预览图

image.png

3、接口

说明:userId参数为1时可以正确获取数据:

apifoxmock.com/m1/2234660-…

url:/myList

请求方式:get

parameter参数:userId = 1

正常响应结果

{
    "error": 0,
    "data": [
        {
            "id": 1,
            "userId": 1,
            "userNickname": "太空先锋",
            "userImageUrl": "https://img95.699pic.com/photo/40034/7177.jpg_wh860.jpg",
            "timestamp": "1980-05-10 04:32:59",
            "text": "宇宙航行不是一个人或某群人的事,这是人类在其发展中合乎规律的历史进程。",
            "images": [
                "http://n.sinaimg.cn/spider2021617/84/w1014h670/20210617/687b-krpikqf6811833.jpg",
                "https://photo.tuchong.com/6571731/f/1154097679.jpg",
                "https://img.xianjichina.com/editer/20201230/image/17d28d1d5958143ef43c1648b1a4c24a.jpg",
                "http://5b0988e595225.cdn.sohucs.com/images/20200427/1588f791e0b54a0182ac0103990d2469.jpeg",
                "https://www.kepuchina.cn/more/202010/W020201005592055625133.jpg",
                "https://pics6.baidu.com/feed/37d3d539b6003af3e95fc85b2c2389561038b633.png@f_auto?token=40225699bd577ae408613509f2239ceb",
                "https://img.guanhai.com.cn/a/10001/202205/845de20e3ffeb3d6ddd80051f4111dc6.jpeg",
                "https://d.ifengimg.com/w951_h521_q90_webp/x0.ifengimg.com/res/2021/884EAC0AC507E34A52A88B0E358AA80BD4DFAFBB_size90_w951_h521.jpeg",
                "http://www.beidou.gov.cn/zy/bdsp/202006/W020200623412798276654.png"
            ],
            "readings": 8,
            "likes": 22
        },
        {
            "id": 7,
            "userNickname": "太空先锋",
            "userImageUrl": "https://img95.699pic.com/photo/40034/7177.jpg_wh860.jpg",
            "timestamp": "1980-05-10 04:32:59",
            "text": "宇指天际,航指航行,也就是航行于天际,寓意一飞冲天。作为亚洲第一航天强国、航天大国,中国宇航工业的发展不仅有利于自身的综合国力的不断提升,更有利于世界天缘政治格局的进一步优化。",
            "images": [
                "http://n.sinaimg.cn/spider2021617/84/w1014h670/20210617/687b-krpikqf6811833.jpg",
                "https://img.xianjichina.com/editer/20201230/image/17d28d1d5958143ef43c1648b1a4c24a.jpg",
                "http://5b0988e595225.cdn.sohucs.com/images/20200427/1588f791e0b54a0182ac0103990d2469.jpeg",
                "https://pics6.baidu.com/feed/37d3d539b6003af3e95fc85b2c2389561038b633.png@f_auto?token=40225699bd577ae408613509f2239ceb",
                "https://img.guanhai.com.cn/a/10001/202205/845de20e3ffeb3d6ddd80051f4111dc6.jpeg",
                "http://www.beidou.gov.cn/zy/bdsp/202006/W020200623412798276654.png"
            ],
            "readings": 16,
            "likes": 0
        }
    ],
    "message": "成功"
}

4、详情

1、用例

  • 文章内容全部显示,如果是从“文章列表”进入详情,文章内容为只读状态。如果从“我的”页面进入详情,文章内容为可编辑状态。
  • 如果从“我的”页面进入详情,右上角有一个确定按钮。
  • 图片不限制数量,全部展示。
  • 如果从“我的”页面进入详情,修改完文章内容后。点击右上角“确定”按钮,调用保存接口,成功后关闭当前页面,同时调用接口刷新“文章列表”、“我的”两个页面。

注意:接口为mock,内容是写死的,并不会真的更新内容。所以更新后调用列表以及详情,文章内容没有变化是正常的。只需要完成刷新操作即可。

2、预览图

image.png

3、接口

1、详情:

说明apifoxmock.com/m1/2234660-…

url:/detail

请求方式:get

parameter参数:id = 1 ~ 10

正常响应结果

{
    "error": 0,
    "data": {
        "id": 1,
        "userId": 1,
        "userNickname": "太空先锋",
        "userImageUrl": "https://img95.699pic.com/photo/40034/7177.jpg_wh860.jpg",
        "timestamp": "1980-05-10 04:32:59",
        "text": "宇宙航行不是一个人或某群人的事,这是人类在其发展中合乎规律的历史进程。",
        "images": [
            "http://n.sinaimg.cn/spider2021617/84/w1014h670/20210617/687b-krpikqf6811833.jpg",
            "https://photo.tuchong.com/6571731/f/1154097679.jpg",
            "https://img.xianjichina.com/editer/20201230/image/17d28d1d5958143ef43c1648b1a4c24a.jpg",
            "http://5b0988e595225.cdn.sohucs.com/images/20200427/1588f791e0b54a0182ac0103990d2469.jpeg",
            "https://www.kepuchina.cn/more/202010/W020201005592055625133.jpg",
            "https://pics6.baidu.com/feed/37d3d539b6003af3e95fc85b2c2389561038b633.png@f_auto?token=40225699bd577ae408613509f2239ceb",
            "https://img.guanhai.com.cn/a/10001/202205/845de20e3ffeb3d6ddd80051f4111dc6.jpeg",
            "https://d.ifengimg.com/w951_h521_q90_webp/x0.ifengimg.com/res/2021/884EAC0AC507E34A52A88B0E358AA80BD4DFAFBB_size90_w951_h521.jpeg",
            "http://www.beidou.gov.cn/zy/bdsp/202006/W020200623412798276654.png"
        ],
        "readings": 8,
        "likes": 16
    },
    "message": "登录成功"
}

2、更新:

url:/update

请求方式:post

body参数

{
        "id": 1,
        "userId": 1,
        "userNickname": "太空先锋",
        "userImageUrl": "https://img95.699pic.com/photo/40034/7177.jpg_wh860.jpg",
        "timestamp": "1980-05-10 04:32:59",
        "text": "宇宙航行不是一个人或某群人的事,这是人类在其发展中合乎规律的历史进程。",
        "images": [
            "http://n.sinaimg.cn/spider2021617/84/w1014h670/20210617/687b-krpikqf6811833.jpg",
            "https://photo.tuchong.com/6571731/f/1154097679.jpg",
            "https://img.xianjichina.com/editer/20201230/image/17d28d1d5958143ef43c1648b1a4c24a.jpg",
            "http://5b0988e595225.cdn.sohucs.com/images/20200427/1588f791e0b54a0182ac0103990d2469.jpeg",
            "https://www.kepuchina.cn/more/202010/W020201005592055625133.jpg",
            "https://pics6.baidu.com/feed/37d3d539b6003af3e95fc85b2c2389561038b633.png@f_auto?token=40225699bd577ae408613509f2239ceb",
            "https://img.guanhai.com.cn/a/10001/202205/845de20e3ffeb3d6ddd80051f4111dc6.jpeg",
            "https://d.ifengimg.com/w951_h521_q90_webp/x0.ifengimg.com/res/2021/884EAC0AC507E34A52A88B0E358AA80BD4DFAFBB_size90_w951_h521.jpeg",
            "http://www.beidou.gov.cn/zy/bdsp/202006/W020200623412798276654.png"
        ],
        "readings": 8,
        "likes": 16
    }

正常响应结果

{
    "error": 0,
    "data": {
        "id": 1,
        "userId": 1,
        "userNickname": "太空先锋",
        "userImageUrl": "https://img95.699pic.com/photo/40034/7177.jpg_wh860.jpg",
        "timestamp": "1980-05-10 04:32:59",
        "text": "宇宙航行不是一个人或某群人的事,这是人类在其发展中合乎规律的历史进程。",
        "images": [
            "http://n.sinaimg.cn/spider2021617/84/w1014h670/20210617/687b-krpikqf6811833.jpg",
            "https://photo.tuchong.com/6571731/f/1154097679.jpg",
            "https://img.xianjichina.com/editer/20201230/image/17d28d1d5958143ef43c1648b1a4c24a.jpg",
            "http://5b0988e595225.cdn.sohucs.com/images/20200427/1588f791e0b54a0182ac0103990d2469.jpeg",
            "https://www.kepuchina.cn/more/202010/W020201005592055625133.jpg",
            "https://pics6.baidu.com/feed/37d3d539b6003af3e95fc85b2c2389561038b633.png@f_auto?token=40225699bd577ae408613509f2239ceb",
            "https://img.guanhai.com.cn/a/10001/202205/845de20e3ffeb3d6ddd80051f4111dc6.jpeg",
            "https://d.ifengimg.com/w951_h521_q90_webp/x0.ifengimg.com/res/2021/884EAC0AC507E34A52A88B0E358AA80BD4DFAFBB_size90_w951_h521.jpeg",
            "http://www.beidou.gov.cn/zy/bdsp/202006/W020200623412798276654.png"
        ],
        "readings": 8,
        "likes": 16
    },
    "message": "登录成功"
}