阅读 3526

Flutter学习之事件循环机制、数据库、网络请求

一、前言

学习了布局实例和交互后,算是对Flutter入门了,基本可以实现一些普通页面搭建和交互效果了。但是这远远还不够,现在App都是需要网络访问的,而今天的目标就是学习IO和网络这一块。

二、Dart中的异步任务消息循环机制

Dart是单线程模型,什么是单线程模型呢?单线程就是在程序执行时,所走的程序路径按照连续顺序排列下来,前面的必须处理好,后面的才会执行(就是同一个时刻只能执行一个操作)。生活中举个例子,在早上上班时,需要指纹打卡,正要打卡的时候,突然来电话,这时候你接电话,接完电话再打卡,从接电话到打卡这操作就是单线程,也就是说,在接电话的时候,打卡这个操作是阻塞的,得要接完电话才能打卡。什么是异步?在计算机领域中,异步指的是线程不需要一直等待下去,而是继续执行下面的操作,不管其他线程的状态,当由消息返回时系统会通知线程进行处理。就好像在平时生活中,我现在需要煮饭,我并不是等着饭煮熟才能去做其他事,而是把电饭锅煮饭按钮按下,然后就可以去看电视,看书等等,等饭好了电饭锅的按钮会跳到保温状态,这时候你就可以吃饭了。这时候我也想到了再AndroidOkHttp的同步和异步请求:

  • 同步:向后台发送一个网络请求,等待后台返回数据结果,然后再发送下一个网络请求
  • 异步:向后台发送一个网络请求,不需要等待后台返回数据,随时可以发送下一个网络请求

但是Flutter中的异步有些不一样,下面慢慢讲述。

1.事件循环体系

1.1.Event-Looper

Dart是单线程模型,并没有主线程/子线程之分,DartEvent loopsEvent Queue模型,而EventLooper将所有的事件依次执行,直接上图:

Events
上图中很直观的反映了Dart中的Event处理模式,当产生一个Event之后,会进入Event queue,而Event loopEventQueue中获取Event并且处理。

1.2.单线程模型

当一个Dart函数开始执行,那么它就会执行到这个函数结束,也就是函数不会被其他代码所打断。这里首先解释一下什么是Dart中的isolateisolate本身是隔离的意思,有自己的内存和单线程控制的实体,因为isolate之间的内存在逻辑是隔离的,isolate的代码是按顺序执行的。在Dart并发可以使用用isolateisolateThread很像,但是isolate之间没有共享内存。一个Dart程序是在Main isolate的Main函数开始,我们平时开发中,默认环境就是Main isolate,App的启动入口main函数就是一个isolate,在Main函数结束后,Main isolate线程开始一个一个处理Event Queue中的每一个Event

mian处理方法

1.3.Dart的消息循环和消息队列

一个DartMain isolate只有一个消息循环(Event Looper)和两个消息队列:Event队列和MicroTask队列。

  1. Event队列包含所有外来事件:I/O,mouse events(鼠标事件),drawing events(绘图),timers(计时器),isolate之间的message等
  2. microTask队列在Dart中是很有必要的,因为有时候事件处理想要在稍后完成一些任务但又希望是在执行下一个事件消息之前。

Event队列包含Dart和系统中其他位置的事件,MicroTask只包含Dart的代码,那么Event Looper处理两个队列的顺序是如下图,当main方法退出后,Event Looper就开始它的工作,首先会以FIFO的顺序执行MicroTask(先执行简短的异步任务),当所有的microtask执行完就会从Event队列去提取事件执行,这样反复,直到两个队列都是空。

队列执行顺序
这里要注意:虽然可以预测知道任务执行顺序,但是无法准确预测事件循环什么时候处理期望的任务。就好像创建了一个延时2s的任务,但是排在你之前的任务结束前事件处理是不会处理这个延时2s任务,也就是执行这个延时任务有可能大于2s。

1.4.通过链接方式指定任务顺序

new Future(() => futureTask)  //异步任务的函数
        .then((d) => "execute value:$d")  //任务执行完后的子任务
        .then((d) => d.length)  //其中d为上个任务执行完后的返回的结果
        .then((d) => printLength(d))
        .whenComplete(() => whenTaskCompelete);  //当所有任务完成后的回调函数
}
复制代码

可以看到,上述代码明确表示前后的依赖关系,可以使用then()()来表明要使用变量就必须要等设置完这个变量。还可以使用whenComplete(),异步完成时的回调。

1.5.Event队列

使用new Future或者new Future.delayed()来向Event队列添加事件,也就是说Future操作是通过Event队列来处理,如下面代码:

//向event队列中添加一个任务
new Future(() {
  //具体任务
});
复制代码

想要在两秒后将任务添加到Event队列

// 两秒以后将任务添加至event队列
new Future.delayed(const Duration(seconds:2), () {
  //任务具体代码
});
复制代码

因为上面说过,上面这个任务想要执行必须满足main方法执行完,Misrotask队列是空的,这个任务之前的任务需要执行完,所以这个任务被执行有可能大于2秒。

1.6.MicroTask队列

scheduleMicrotask(() {
  // 具体逻辑
});
复制代码

上面就是将一个任务加到MicroTask队列中去。

1.7.例子1

import 'dart:async';
main() {
  print('main #1 of 2');
  scheduleMicrotask(() => print('microtask #1 of 2'));

  new Future.delayed(new Duration(seconds:1),
                     () => print('future #1 (delayed)'));
  new Future(() => print('future #2 of 3'));
  new Future(() => print('future #3 of 3'));

  scheduleMicrotask(() => print('microtask #2 of 2'));

  print('main #2 of 2');
}
复制代码

输出结果:

main #1 of 2
main #2 of 2
microtask #1 of 2
microtask #2 of 2
future #2 of 3
future #3 of 3
future #1 (delayed)
复制代码

上面执行顺序:main方法 ->Microtask队列->Event队列(先 new Future 后new Future.delay),下面直接拿官方的例子实践一下:

1.8.例子2

import 'dart:async';
main() {
  print('main #1 of 2');
  scheduleMicrotask(() => print('microtask #1 of 3'));

  new Future.delayed(new Duration(seconds:1),
      () => print('future #1 (delayed)'));

  new Future(() => print('future #2 of 4'))
      .then((_) => print('future #2a'))
      .then((_) {
        print('future #2b');
        scheduleMicrotask(() => print('microtask #0 (from future #2b)'));
      })
      .then((_) => print('future #2c'));

  scheduleMicrotask(() => print('microtask #2 of 3'));

  new Future(() => print('future #3 of 4'))
      .then((_) => new Future(
                   () => print('future #3a (a new future)')))
      .then((_) => print('future #3b'));

  new Future(() => print('future #4 of 4'));
  scheduleMicrotask(() => print('microtask #3 of 3'));
  print('main #2 of 2');
}
复制代码

输出结果:

main #1 of 2
main #2 of 2
microtask #1 of 3
microtask #2 of 3
microtask #3 of 3
future #2 of 4
future #2a
future #2b
future #2c
microtask #0 (from future #2b)
future #3 of 4
future #4 of 4
future #3a (a new future)
future #3b
future #1 (delayed)
复制代码

上面两个小例子会加深对事件消息的理解。

三、Dart中的异步支持

因为Dart是单线程语言,当遇到延迟的运算(I/O操作),线程中顺序执行的运算就会阻塞,那就app上,用户操作就会感到卡顿,于是通常用异步处理来解决这个问题,当遇到需要延迟的运算时,就会放入延迟运算的队列中,先把不需要延迟的运算先执行,最后再来处理延迟运算。Dart类库有非常多的返回Future或者Stream对象的函数,这些函数被称为异步函数;它们会在设置好一些需要消耗一定时间的操作之后返回,比如I/O操作,而不是等到这个操作完成。

1.Future

什么是Future,顾名思义,表示一件将来会发生的事情(也就是不会立即执行),将来可以从Future中取到一个值,当一个方法返回一个Future的事情,发生两件事:

  1. 这个方法将某件事情排队,返回一个未完成的Future
  2. 这个方法事情完毕后,Future的状态会变成已经完成,这个时候可以取到这件事情的返回值。

Future表示一个异步操作的最终完成(或失败)及其结果值的表示,简单来说,它就是用来处理异步操作的,异步处理成功就执行成功的操作,异步处理失败就捕获错误或者停止后续操作,一个Future只会对应一个结果,要么成功,要么失败。

1.1.Future.then

main() {
  create();
}

//模执行延时任务
void create(){
   //延迟三秒执行
   Future.delayed(new Duration(seconds: 3),(){
     return "This is data";
   }).then((data){
     print(data);
   });
}
复制代码

输出结果如下:

This is data
复制代码

上面可以发现,使用Future.delayed创建一个延时任务,当三秒后通过thendata接收了这个所返回的This is data这个字符串的值。下面读取一个文件,先创建一个文件:

文件存储位置
下面读取内容看看:

main() {
  create();
}
void create(){
   //延迟三秒执行
   var file = File("/Users/luguian/Downloads/flutter第五天/flutter.rtf");
   //定义了返回结果值为String类型
   Future<String> data = file.readAsString();
   //返回文件内容
   data.then((text){
     //打印文件内容
     print(text);
   });
   print("I love Android");
}
复制代码
I love Android -->先打印I love Android
{\rtf1\ansi\ansicpg936\cocoartf1561\cocoasubrtf600
{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset134 PingFangSC-Regular;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
\paperw11900\paperh16840\margl1440\margr1440\vieww10800\viewh8400\viewkind0
\pard\tx566\tx1133\tx1700\tx2083\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0

\f0\fs24 \cf0 I love flutter --->文字内容
\f1 ;}
复制代码

可以发现,先输出I love Android然后读取文件这种超时的操作会后执行,也就是读取文件内容是将来执行的then接收异步并打印出结果。注意:Future并不是并行执行的

1.2.Future.catchError

当异步任务发生错误,可以在catchError中捕获错误,例子如下:

Future.delayed(new Duration(seconds: 3),(){
   throw AssertionError("This is a Error");
}).then((data){
  //这是成功的逻辑
  print("success");
}).catchError((e){
  //失败会走到这里
  print(e);
});
复制代码

输出结果如下:

Assertion failed
复制代码

可以发现,在异步任务中抛出了一个异常,then的回调函数不会执行,反而catchError函数被调用,当然并不是只有catchError才能捕获错误,then方法有一个可选的参数onError,可以用它来捕获异常:

Future.delayed(new Duration(seconds: 3),(){
   throw AssertionError("This is a Error");
}).then((data){
  //这是成功的逻辑
  print("success");
},onError:(e){
  print(e);   
});
复制代码

输出结果:

Assertion failed
复制代码

1.3.Future.whenComplete

有很多时候,当异步任务无论成功或者失败都需要做一些事的场景,如在网络请求前弹出加载进度框,在请求结束后关闭进度框,下面用whenComplete进行回调,例子如下:

Future.delayed(new Duration(seconds: 3),(){
   throw AssertionError("This is a Error");
}).then((data){
  //这是成功的逻辑
  print("success");
},onError:(e){
  //这是失败的逻辑
  print(e);
}).whenComplete((){
  print("无论失败,或者成功都会走到这");
});

}
复制代码

输出结果如下:

Assertion failed
无论失败,或者成功都会走到这
复制代码

1.4.Future.wait

有时候,需要等待多个异步任务都执行结束后才进行一些操作,如有一个界面,需要从两个接口获取数据,获取成功后,将两个数据进行处理后显示在UI界面上,这时候,Future.wait派出用上了,它接收一个Future数组参数,只有数组中所有的Future执行成功后,就会触发then回调,当然,只要有一个Future执行失败就会触发错误回调,下面实现一下当两个异步任务都执行成功时,将结果打印出来:

Future.wait([
  //3秒后返回结果
  Future.delayed(new Duration(seconds: 3),(){
    return "Android";
  }),
  //4秒后返回结果
  Future.delayed(new Duration(seconds: 4),(){
    return " And Future";
  })
]).then((data){
  //成功逻辑
  print(data[0] + data[1]);
}).catchError((e){
  //捕捉错误
  print(e);
});
}
复制代码

输出结果如下:

Android And Future
复制代码

可以看到当两个异步任务完成才会回调then函数。

2.Async/await

使用Async/await也是可以实现异步操作,下面直接上例子:

main() {
  create();
}
void create(){
   String data =  getData();
   print(data);
   print("I love Future");
}
getData() async{
  return await "I love Android";
}
复制代码

运行上面代码,报错了:

type 'Future<dynamic>' is not a subtype of type 'String'
复制代码

报的是类型不匹配?为什么呢?经过一番搜查,发现getData是一个异步操作函数,它的返回值是一个await延迟执行的结果。在Dart中,有await标记的运算,其结果值是一个Future对象,Future并不是String类型,就报错了。那么怎么才正确获得异步的结果呢?Dart规定async标记的函数,只能由await来调用,下面改成这样:

main() {
  create();
}

void create() async{
   String data =  await getData();
   print(data);
   print("I love Future");

}
getData() async{
  return await "I love Android";
}
复制代码

下面直接去掉async函数包装,直接在getData方法里对data进行赋值:


String data;

main() {
  create();
}

void create(){
   getData();
   print("I love Future");

}
getData() async{
  data =  await "I love Android";
  print(data);
}
复制代码

上面输出结果是:

I love Future
I love Android
复制代码

可以发现,先输出的是I love Future后面再输出I love Android,可以发现当函数被async修饰时,会先去执行下面的操作,当下面的操作执行完,然后再执行被async修饰的方法。async用来表示函数是异步的,定义的函数会返回一个Future对象,await后面是一个Future,表示等待该异步任务完成,异步完成后才会往下走。要注意以下几点:

  1. await关键字必须在async函数内部使用,也就是加await不加async会报错。
  2. 调用async函数必须使用await关键字,如果加async不加await会顺序执行代码。

下面再上例子:

main() {
  _startMethod();
  _method_C();

}

_startMethod() async{
  _method_A();
  await _method_B();
  print("start结束");
}
_method_A(){
  print("A开始执行这个方法~");

}

_method_B() async {
  print("B开始执行这个方法~");
  await  print("后面执行这句话~");
  print("继续执行这句哈11111~");
}

_method_C(){
  print("C开始");
}
复制代码

结果如下:

A开始执行这个方法~
B开始执行这个方法~
后面执行这句话~
C开始
继续执行这句哈11111~
start结束
复制代码
  1. 当使用async作为方法名后缀声明时,说明这个方法的返回值是一个Future
  2. 当执行到该方法代码用await关键字标注时,会暂停该方法其他部分执行;
  3. await关键字引用的Future执行完成,下一行代码会立即执行。

也就是首先执行_startMethod这个方法用async声明了,因为方法里调用了_method_A,所以先输出print("A开始执行这个方法~");,后面执行_method_B(),这个方法用await关键字声明,所以会暂停print("start结束");的执行,然后继续执行_method_B() print("B开始执行这个方法~");输出,下一行遇到await关键字,会暂停其他代码的执行。当await关键字引用的Future执行完成(也就是执行print("后面执行这句话~"),_method_C()方法会立即执行,然后执行继续执行这句哈11111~,最后执行print("start结束");

3.Stream

Stram是接收异步事件数据,和Future不同的是,它可以接收多个异步操作的结果,那么Stram常用于在多次读取数据的异步任务场景,直接上例子:

void create(){

  Stream.fromFutures([
    //2秒后返回结果
    Future.delayed(new Duration(seconds: 2),(){
      return "Android";
    }),

    //3秒后抛出一个异常
    Future.delayed(new Duration(seconds: 3),(){
      return AssertionError("error");
    }),

    //4秒后返回结果
    Future.delayed(new Duration(seconds: 4),(){
      return "Flutter";
    })

  ]).listen((result){
    //打印接收的结果
     print(result);
  },onError: (e){
     //错误回调
     print(e.message);

  },onDone: (){

  });

}
复制代码

上面可以发现Stream可以通过触发成功或者失败传递结果或者错误。

四、文件操作

有很多时候需要将文件保存到本地,这时候就需要用文件读写接口来实现,PathProvider插件提供一种平台透明的方式来访问设备文件系统上的常用位置。该类当前支持两个文件系统位置:

  • 临时目录:系统可随时清除的临时目录(缓存)。在iOS上,这对应于NSTemporaryDirectory()返回的值。在Android上,这是getCacheDir()返回的值。
  • 文档目录:应用程序的目录,用于存储只有自己可以访问的文件,只有当应用程序被卸载时,系统才会清除目录。在iOS上,这对应于NSDocumentDirectory。在Android上,这是AppData目录。

Flutter里实现文件读写,需要使用path_providerDart里的I/O模块,两者的职责并不一样,path_provider是负责查找iOS或者Android下的目录文件,而I/O是负责文件的读写操作。

1.获取本地路径

下面使用path_provider来查找本地的路径,首先在pubspec.xml文件添加依赖:

dependencies:
  flutter:
    sdk: flutter
  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  path_provider: ^0.4.1 -->添加依赖
复制代码

或者临时目录,文档目录,sd卡目录如下:

import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
....
class _LoadFileState extends State<LoadFile>{
   @override
   void initState(){
     super.initState();
   }
   
   @override
   Widget build(BuildContext context){
     return new Scaffold(
       appBar: new AppBar(
         title: new Text("LoadFile"),
       ),
       body: new Center(
         child: RaisedButton(
             child: Text("获取文件路径"),
             //点击调用获取文件路径方法
             onPressed: loadPath,
         ),
       ),
     );
   }
}

loadPath() async{
    try{
      //临时目录
      var _tempDir = await getTemporaryDirectory();
      //获取具体路径
      String tempDirPath = _tempDir.path;
      //文档目录
      var _document = await getApplicationDocumentsDirectory();
      String documentPath = _document.path;
      //sd卡目录
      var _sdCard = await getExternalStorageDirectory();
      String sdCardPath = _sdCard.path;

      //打印路径
      print("临时目录:"+ tempDirPath);
      print("文档目录:"+ documentPath);
      print("sd卡目录:"+ sdCardPath);

    }catch(err){
       print(err);
    }
}
复制代码

输出结果(Android)如下:

I/flutter (19375): 临时目录:/data/user/0/com.example.loadflie/cache
I/flutter (19375): 文档目录:/data/user/0/com.example.loadflie/app_flutter
I/flutter (19375): sd卡目录:/storage/emulated/0
复制代码

2.读取本地文件内容

读取文件少不了权限的问题,在Dart Packages可以找到simple_permissions这个库来简化申请权限的步骤,按照上面说明跟着操作就可以:

权限库操作
上面的意思是在AndroidManifestInfo.plist文件下添加权限,身为Android coder对AndroidManifest这个文件很熟悉,这个文件是对Android而言,而Info.plist应该是对于iOS而言,那下面先在Android上试试看,首先,在pubspec.yaml上添加依赖:

simple_permissions: ^0.1.9
复制代码

记得点击Packages get命令。 接着在AndroidManifest清单文件上添加对文件的读写权限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
复制代码

下面在手机sd card内部存储新建一个txt文件,尝试获取其内容:

import 'package:simple_permissions/simple_permissions.dart';//记得加上这句话
...
//读取文件方法
readData() async {
  try {
    //申请读文件的权限
    var permission =
    SimplePermissions.requestPermission(Permission.ReadExternalStorage);
    var sdCardPath = getExternalStorageDirectory();
    //当获取到路径的时候
    sdCardPath.then((filePath) {
      //获得读取文件权限
      permission.then((permission_status) async {
        //获取文件内容
        var data = await File(filePath.path + "/flutter.txt").readAsString();
        print(data);
      });
    });
  } catch (e) {
    print(e);
  }
}
复制代码

按钮点击方法改为readData:

        child: RaisedButton(
          child: Text("获取文件路径"),
          onPressed: readData
        ),
复制代码

点击按钮结果运行:

权限弹出框
选择始终运行: 输出就是flutter.txt文件内容:

I/flutter (24038): flutter is very good.
复制代码

注意如果不加读写权限,会抛出异常:

I/flutter (25428): FileSystemException: Cannot open file, path = '/storage/emulated/0/flutter.txt' (OS Error: Permission denied, errno = 13)
复制代码

3.写入文件操作

//把内容写入文件操作
writeData() async{
  try {
    //申请读文件的权限
    var permission =
    SimplePermissions.requestPermission(Permission.WriteExternalStorage);
    var sdCardPath = getExternalStorageDirectory();
    //当获取到路径的时候
    sdCardPath.then((filePath) {
      //获得读取文件权限
      permission.then((permission_status) async {
        //把内容写进文件
        var data = await File(filePath.path + "/flutter.txt").writeAsString("点滴之行,看世界");
        print(data);
      });
    });
  } catch (e) {
    print(e);
  }
}
复制代码

打开sd card的flutter.txt文件看看内容:

写入文件内容
发现,把之前的内容覆盖了!那么如何实现所写入的内容不覆盖原来文件的内容呢?这时候需要用到append模式,很简单,把默认的FileMode mode: FileMode.write方式改为FileMode mode: FileMode.append,代码如下:

        //把内容写进文件 现在以追加的方式
        var data = await File(filePath.path + "/flutter.txt").writeAsString("Flutter is very good",
             mode: FileMode.append);
复制代码

运行结果:

追加方式填写内容
好了,简单的读写文件就实现了。

五、sqflite数据库

AndroidiOS中都会有SQLite,那么Flutter有没有呢?答案是肯定有的。Flutter中的SQLite数据库是同时支持AndroidiOS的,它的名字叫sqflite ,支持事务和批量操作,支持插入/查询/更新/删除操作等,是轻量级的关系型数据库。 下面先简单实现一个登录界面,进行简单的数据操作:

//用无状态控件显示
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //主题色
      theme: ThemeData(
          //设置为蓝色
          primarySwatch: Colors.red),
      //这是一个Widget对象,用来定义当前应用打开的时候,所显示的界面
      home: DataBaseWidget(),
    );
  }
}

//主框架
class DataBaseWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new _DataBaseState();
  }
}

class _DataBaseState extends State<DataBaseWidget> {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      //appBar
      appBar: AppBar(
        title: Text("Sqlite简单操作"),
        //标题居中
        centerTitle: true,
      ),
      body: new ListView(
        children: <Widget>[
          //用户输入用户信息widget
          Padding(
            padding: const EdgeInsets.only(left: 16, right: 16),
            child: InputMessageWidget(),
          ),
          //数据库表的一些基本操作,增,删,改,查
          Padding(
            padding: const EdgeInsets.all(16),
            child: SqliteHandleWidget(),
          ),
        ],
      ),
    );
  }
}
复制代码

用户输入信息的Widget如何:

//用户名和密码
class InputMessageWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //这个是为了用户输入结束后,让密码输入框获取到焦点
    FocusNode secondTextFieldNode = FocusNode();
    return Column(
      children: <Widget>[
        TextField(
          //文字内容改变触发
          onChanged: (user) {
            //获取用户名
            username = user;
          },
          //输入法装饰器
          decoration: InputDecoration(
              //标签
              labelText: '名字',
              //hint 提示用户输入什么
              hintText: '请输入英文或者数字'),
          //最大为一行
          maxLines: 1,
          //文字提交触发
          onSubmitted: (result) {
            FocusScope.of(context).reparentIfNeeded(secondTextFieldNode);
          },
        ),
        TextField(
          onChanged: (pwd) {
            //获取用户密码
            password = pwd;
          },
          //是否隐藏输入 false 表示不隐藏,true表示隐藏
          obscureText: true,
          maxLines: 1,
          decoration: InputDecoration(
            labelText: '密码',
            hintText: '请输入密码',
          ),
          //键盘输入类型
          keyboardType: TextInputType.text,
          onSubmitted: (data) {},
        ),
      ],
    );
  }
}
复制代码

对数据库表操作的按钮布局如下:

//数据库组件操作
class SqliteHandleWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new _SqliteHandleWidgetState();
  }
}

class _SqliteHandleWidgetState extends State<SqliteHandleWidget> {
  //数据库名称
  String myDataBase = "usermessage.db";

  //数据库路径
  String myDataBasePath = "";

  //数据库中的表 简单一点,就创建三个字段,分别是主键,用户名,密码
  String sql_createUserTable = "CREATE TABLE user("
      "id INTEGER PRIMARY KEY,"
      "username TEXT,"
      "password TEXT)";

  //查找数据库表的数目
  String sql_queryCount = 'SELECT COUNT(*) FROM user';

  //具体查找数据库表的所有信息
  String sql_queryMessage = 'SELECT * FROM user';

  //这是从数据库表返回数据
  var _data;

  @override
  Widget build(BuildContext context) {
    return Column(
      //交叉轴设置中间
      crossAxisAlignment: CrossAxisAlignment.center,
      children: <Widget>[
        Container(
          height: 40.0,
          child: RaisedButton(
            textColor: Colors.black,
            child: Text("创建数据库表"),
            onPressed: null,
          ),
        ),
          Row(
            //主轴方向中心对齐
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              new RaisedButton(
                  textColor: Colors.black,
                  child: new Text('增'),
                  onPressed: null),
              new RaisedButton(
                  textColor: Colors.black,
                  child: new Text('删'),
                  onPressed: null),
              new RaisedButton(
                  textColor: Colors.black,
                  child: new Text('改'),
                  onPressed: null),
            ],
        ),
        Row(
          //主轴方向中心对齐
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new RaisedButton(
                textColor: Colors.black,
                child: new Text('查条数'),
                onPressed: null),
            new RaisedButton(
                textColor: Colors.black,
                child: new Text('查信息'),
                onPressed: null),
          ],
        ),
        Padding(
          padding: const EdgeInsets.all(16.0),
          child: new Text('具体结果是:$_data'),
        ),
      ],
    );
  }
}
复制代码

在上面_SqliteHandleWidgetState赋值数据库名字为usermessage.db,创建数据库表user语句很简单,就三个字段,分别是主键,用户名,用户密码,界面如下:

数据库表的界面
界面弄好了,下面就一步一步来。

1.创建数据库和数据表

首先添加依赖:可以到Dart包管理网站去查找sqlite依赖最新版本。

  sqflite: ^1.1.0
复制代码

并在文件引入:

import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
复制代码

注意:对于数据库的操作都是耗时操作,都要通过异步来处理。

  //创建数据库
  Future<String> createDataBase(String db_name) async {
    //在文档目录建立
    var document = await getApplicationDocumentsDirectory();
    //获取路径 join是path包下的方法,就是将两者路径连接起来
    String path = join(document.path, db_name);
    //逻辑是如果数据库存在就把它删除然后创建
    var _directory = new Directory(dirname(path));
    bool exists = await _directory.exists();
    if (exists) {
      //必存在 这里是为了每次创建数据库表先表删除则删除数据库表
      await deleteDatabase(path);
    } else {
      try {
        //不存在则创建目录  如果[recursive]为false,则只有路径中的最后一个目录是
        //创建。如果[recursive]为真,则所有不存在的路径
        //被创建。如果目录已经存在,则不执行任何操作。
        await new Directory(dirname(path)).create(recursive: true);
      } catch (e) {
        print(e);
      }
    }
    return path;
  }


  //创建数据库表方法
  cratedb_table() async {
    //得到数据库的路径
    myDataBasePath = await createDataBase(myDataBase);
    //打开数据库
    Database my_db = await openDatabase(myDataBasePath);
    //创建数据库表
    await my_db.execute(sql_createUserTable);
    //关闭数据库
    await my_db.close();
    setState(() {
      _data = "创建usermessage.db成功,创建user表成功~";
    });
}
复制代码

给按钮添加点击方法:

          child: RaisedButton(
            textColor: Colors.black,
            child: Text("创建数据库表"),
            onPressed: cratedb_table,
          ),
复制代码

运行,安装完apk,用Device File Exploder来看看内部存储文件:

内部存储一
下面点击创建数据库,后synchronize来刷新一下:

创建了数据库表
发现在app_flutter下多了usermessage.db文件,确实数据库创建成功了,那继续下面的操作。

2.增加数据

下面实现增加数据,可以用rawInsert或者db.insert方式对数据库表数据进行增加(插入),实际上都是通过insert into方式来插入数据表,下面就用rawInsert方式来增加一条数据:

//增加方法
addData() async {
    //首先打开数据库
    Database my_db = await openDatabase(myDataBasePath);
    //插入数据
    String add_sql = "INSERT INTO user(username,password) VALUES('$username','$password')";
    await my_db.transaction((tran) async{
       await tran.rawInsert(add_sql);
    });
    //关闭数据库
    await my_db.close();
    setState(() {
      _data = "增加一条数据成功,名字是:$username,密码是:$password";
    });
}
复制代码

3.查询具体数据

为了配合增加数据,把查询数据库表的功能实现:

//查询具体数值
queryDetail() async{
    //打开数据库
    Database  my_db = await openDatabase(myDataBasePath);
    //将数据放到集合里面显示
    List<Map> dataList = await my_db.rawQuery(sql_queryMessage);
    await my_db.close();
    setState(() {
       _data = "具体数据详情如下:$dataList";
    });
}
复制代码

查询数据表很简单,实际上只用rawQuery这个方法,把增加和查询方法绑定到按钮点击上:

new RaisedButton(
    textColor: Colors.black, child: new Text('改'), onPressed: null),
....
new RaisedButton(
    textColor: Colors.black,
    child: new Text('查信息'),
    onPressed: queryDetail),
复制代码

验证结果,流程是:

  1. 先输入用户名和密码
  2. 点击增加
  3. 点击查信息: 运行结果如下:

增加的结果

4.删除数据

下面实现删除数据:

//删除一条数据
delete() async {
    Database my_db = await openDatabase(myDataBasePath);
    //根据id来删除 也可以根据其他信息来删除 例如名字
    String delete_ssql = "DELETE FROM user WHERE id = ?";
    //返回所更改的数目
    int delete_count = await my_db.rawDelete(delete_ssql,['1']);
    //关闭数据库
    await my_db.close();
    //状态更新
    setState(() {
      if(delete_count == 1){
         _data = "删除成功~";
      } else {
        _data = "删除失败,请看错误日志~";
      }
    });
}
复制代码

记得给删除按钮绑定方法,运行结果就不贴了。

5.修改数据

修改数据我相信在平时开发中是用的最频繁的操作了,直接上实现例子:

//修改数据方法
update() async{
   //数据库
   Database my_db = await openDatabase(myDataBasePath);
   String update_sql = "UPDATE user SET username = ? WHERE id = ?";
   await my_db.rawUpdate(update_sql,['paul','1']);
   await my_db.close();

   setState(() {
      _data = "数据修改成功,请查阅~";
   });

}
复制代码

上面用了rawUpdate对数据库表进行内容数据更新,也可以用db.update来更新,自己可以根据需求变更去修改固定字段或者整条数据。上面我是根据id这个条件来修改一条数据,将id为1的数据的名字改为paul

6.查询条数

//查询有几条
query_num() async{
  //数据库
  Database my_db = await openDatabase(myDataBasePath);
  //用sqflite包的方法firstInValue
  int data_count = Sqflite.firstIntValue(await my_db.rawQuery(sql_queryCount));
  await my_db.close();
  setState(() {
    _data = "数据条数:$data_count";
  });
}
复制代码

对本地数据库的基本操作实现了一遍,下面学习网络请求操作。

六、网络请求操作

Flutter的请求网络有多种方式,一种是使用dart io中的HttpClient发起的请求,一种是使用dio库,另一种是使用http库,先学一下getpostputdelete就等后面用到在学。下面就实践:

1.dart io发起的请求

1.1.get请求

import 'dart:io';//导IO包
import 'dart:convert';//解码和编码JSON
void main() {
  _get();
}

_get() async{
  var responseBody;
  //1.创建HttpClient
  var httpClient = new HttpClient();
  //2.构造Uri
  var requset = await httpClient.getUrl(Uri.parse("http://gank.io/api/data/%E7%A6%8F%E5%88%A9/10/1"));
  //3.关闭请求,等待响应
  var response = await requset.close();
  //4.进行解码,获取数据
  if(response.statusCode == 200){
      //拿到请求的数据
      responseBody = await response.transform(utf8.decoder).join();
      //先不解析打印数据
      print(responseBody);
  }else{
    print("error");
  }
  
}
复制代码

结果如下:

请求一返回的数据

1.2.post请求

_post() async{
  var responseBody;
  //1.创建HttpClient
  var httpClient = new HttpClient();
  //2.构造Uri
  var requset = await httpClient.postUrl(Uri.parse("http://www.wanandroid.com/user/login?username=1&password=123456"));
  //3.关闭请求,等待响应
  var response = await requset.close();
  //4.进行解码,获取数据
  if(response.statusCode == 200){
  //拿到请求的数据
  responseBody = await response.transform(utf8.decoder).join();
  //先不解析打印数据
    print(responseBody);
  }else{
    print("error");
  }

}
复制代码

返回结果如下:

post请求

2.dio请求

dio是一个强大的Dart Http请求库,支持Restful APIFormData、拦截器、错误处理、转换器、设置Http代理、请求取消、Cookie管理、文件上传和下载、超时等。在pub.flutter-io.cn/packages搜最新的依赖包,这个网址太好用,你想搜一些三方库里面都有:

dio最新依赖
pubspec.yaml添加依赖:

dio: ^2.0.14
复制代码

导入依赖:

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

2.1.get请求

//dio get请求
dio_get() async{
  try{
      Response response;
      //等待返回response
      response = await Dio().get("http://gank.io/api/data/%E7%A6%8F%E5%88%A9/10/1");
      if(response.statusCode == 200){
        print(response);
      }else{
        print("error");
      }
  }catch(e){
     print(e);

  }

}
复制代码

2.2.post请求

dio_post() async{
  try{
    Response response;
    response = await Dio().post("http://www.wanandroid.com/user/login?username=1&password=123456");
    if(response.statusCode == 200){
      print(response);
    }else{
      print("error");
    }
  }catch(e){
    print(e);
  }
}
复制代码

效果同样是ok的。

3.http库

继续去上面链接搜最新的包,是http 0.12.0+1,在pubspec.yaml下添加依赖,在文件导入包:

import 'package:http/http.dart' as my_http;
复制代码

上面这次导入库的方式有一点点区别,多了as这个关键字,这是什么意思呢?通过as是为了解决变量名冲突的方法,因为导入不同的库有可能遇到不同库之间因为导入变量名冲突的问题。

3.1.get请求

//http库的get请求方式
http_get() async{
  try{
    //因为导入http 用了as xxx方式,所以对象请求都用xxx.get方式
    var response = await my_http.get("http://gank.io/api/data/%E7%A6%8F%E5%88%A9/10/1");
    if(response.statusCode == 200){
      //打印返回的数据
      print(response.body);
    }else{
      print("error");
    }
  }catch(e){
    print(e);
  }
}
复制代码

3.2.post请求

//http库的post请求方式
http_post() async{
  try{
    //因为导入http 用了as xxx方式,所以对象请求都用xxx.get方式
    var response = await my_http.post("http://www.wanandroid.com/user/login?username=1&password=123456");
    if(response.statusCode == 200){
      //打印返回的数据
      print(response.body);
    }else{
      print("error");
    }
  }catch(e){
    print(e);
  }
}
复制代码

以上三种库的getpsot方式都实践了一遍,在平时开发中最好用dio库和http库,因为dart io中是使用HttpClient发起的请求,HttpClient本身功能较弱,很多常用功能不支持。

七、JSON

现在很难想象移动应用程序不需要与后台交互或者存储结构化数据。现在开发,数据传输方式基本都是用JSON,在Flutter中是没有GSON/Jackson/Moshi这些库,因为这些库需要运行时反射,在Flutter是禁用的。运行时反射会干扰Dart的_tree shaking_。使用_tree shaking_,可以在发版是"去除"未使用的代码,来优化软件的大小。由于反射会默认使用所有代码,因此_tree shaking_会很难工作,这些工具无法知道哪些widget在运行时未被使用,因此冗余代码很难剥离,使用反射时,应用尺寸无法轻松进行优化,虽然不能在Flutter使用运行时反射,但有些库提供了类型简单易用的API,但它们是基于代码生成的。下面学学在Flutter中如何操作JSON数据的使用JSON有两个常规策略:

  1. 手动序列化和反序列化
  2. 通过代码生成自动序列化和反序列化 不同的项目有不同的复杂度和场景,针对于小的项目,使用代码生成器可能会杀猪用牛刀了。对于具有多个JSON model的复杂应用程序,手动序列化可能会比较繁琐,且容易出错。

1.手动序列化JSON

Flutter中基本的JSON序列化非常简单,Flutter有一个内置的dart:convert库,其中包含一个简单的JSON解码器和编码器。下面简单实现一下:

1.1.内连序列化JSON

首先记得导库:

import 'dart:convert';
复制代码

然后根据字符串解析:

//内连序列化JSON
decodeJson() {
    var data= '{"name": "Knight","email": "Knight@163.com"}';
    Map<String,dynamic> user = json.decode(data);
    //输出名字
    print("Hello,my name is ${user['name']}");
    //输出邮箱
    print("Hello,This is my email ${user['email']}");
}
复制代码

结果输出:

I/flutter ( 5866): Hello,my name is Knight
I/flutter ( 5866): Hello,This is my email Knight@163.com
复制代码

这样,可以获得我们想要的数据了,我觉得这种方法很实用又能简单理解,但是不幸的是,JSON.decode()仅返回一个Map<String,dynamci>,这意味着当直到运行才知道值的类型,这种方法会失去大部分静态类型语言特性:类型安全、自动补全和编译时异常。这样的话,代码变得非常容易出错,就好像上面我们访问name字段,打字打错了,达成namr。但是这个JSON在map结构中,编译器不知道这个错误的字段名(编译时不会报错)。为了解决所说的问题,模型类中序列化JSON的作用出来了。

1.2.模型类中序列化JSON

通过引入一个简单的模型类(model class)来解决前面提到的问题,建立一个User类,在类内部有两个方法:

  1. User.fromJson构造函数,用于从一个map构造出一个User实例map structure
  2. toJson方法,将User实例化一个map 这样调用的代码就具有类型安全、自动补全和编译时异常,当拼写错误或字段类型视为其他类型,程序不会通过编译,那就避免运行时崩溃。
1.2.1.user.dart

新建一个model文件夹,用来放实体,在其文件下新建User.dart:

class User {
  final String name;
  final String email;

  User(this.name, this.email);

  User.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        email = json['email'];

  Map<String, dynamic> toJson() =>
    {
      'name': name,
      'email': email,
    };
}
复制代码

调用如下:

import 'model/User.dart';//记得添加
....
//使用模型类反序列化
decodeModelJson(){
  var data= '{"name": "Knight","email": "Knight@163.com"}';
  Map userMap = json.decode(data);
  var user = new User.fromJson(userMap);
  //打印出名字
  print("Hello,my name is ${user.name}");
  //打印出邮箱
  print("Hello,my name is ${user.email}");
}
复制代码

把序列化逻辑到移到模型本身内部,采用这种方法,反序列化数据就很简单了。序列化一个user,只是将User对象传递给该JSON.encode方法:

//序列化一个user
encodeModelJson(){
  var user = new User("Knight","Knight163.com");
  String user_json = json.encode(user);
  print(user_json);
}
复制代码

结果输出:

I/flutter ( 6684): {"name":"Knight","email":"Knight163.com"}
复制代码

2.使用代码生产库序列化JSON

下面使用json_serializable package包,它是一个自动化的源代码生成器,可以为开发者生成JSON序列化魔板。

2.1.添加依赖

要包含json_serializable到项目中,需要一个常规和两个开发依赖项,开发依赖项是不包含在应用程序源代码中的依赖项:

dependencies:
  # Your other regular dependencies here
  json_annotation: ^2.0.0

dev_dependencies:-->开发依赖项
  # Your other dev_dependencies here
  build_runner: ^1.1.3 -->最新版本1.2.8 因为我sdk版本比较低 所以用低版本
  json_serializable: ^2.0.2
复制代码

2.2.代码生成

有两种运行代码生成器的方法:

  1. 一次性生成,在项目根目录运行flutter packages pub run build_runner build,可以在需要为我们的model生成json序列化代码。这触发一次性构建,它通过源文件,挑选相关的并为它们生成必要的序列化代码。这个非常方便,但是如果我们不需要每次在model类中进行更改都要手动运行构建命令的话会更好。
  2. 持续生成,使用_watcher_可以使源代码生成的过程更加方便,它会监视项目中文化的变化,并在需要时自动构建必要的文件,通过flutter packages pub run build_runner watch在项目根目录运行启动_watcher_,只需启动一次观察器,然后并让它在后台运行,这是安全的。

将上面的User.dart修改成下面:

import 'package:json_annotation/json_annotation.dart';
part 'User.g.dart';-->一开始爆红
//这个标注是告诉生成器,这个类是需要生成Model类的
@JsonSerializable()
class User{
  User(this.name, this.email);

  String name;
  String email;
  
  factory User.fromJson(Map<String, dynamic> json){--->一开始爆红
     return _$UserFromJson(json);
  }
  
  Map<String, dynamic> toJson() { --->一开始爆红
    return _$UserToJson(this);
  }
}
复制代码

下面就用一次性生成命令,在项目根目录打开命令行执行:

执行命令

最后生成的命令
最后发现会在当前目录生成User.g.dart文件:

生成dart文件
里面的内容可以自己去看看看,就是反序列化/序列化的操作。注意:没生成User.g.dart执行多几次命令即可。 最后通过json_serializable方式反序列化JSON字符串,不需要对先前代码修改:

2.3.反序列化

  var data= '{"name": "Knight","email": "Knight@163.com"}';
  Map userMap = json.decode(data);
  var user = new User.fromJson(userMap);
  //打印出名字
  print("Hello,my name is ${user.name}");
  //打印出邮箱
  print("Hello,my name is ${user.email}");
复制代码

2.4.序列化

  var user = new User("Knight","Knight163.com");
  String user_json = json.encode(user);
  print(user_json);
复制代码

结果是跟上面一样,不过这种方式额外多了生成一个文件...

八、例子

下面实现一个简单例子,效果图如下:

最终效果图
返回的json格式是如下:

{
	"error": false,
	"results": [{
		"_id": "5c6a4ae99d212226776d3256",
		"createdAt": "2019-02-18T06:04:25.571Z",
		"desc": "2019-02-18",
		"publishedAt": "2019-02-18T06:05:41.975Z",
		"source": "web",
		"type": "\u798f\u5229",
		"url": "https://ws1.sinaimg.cn/large/0065oQSqly1g0ajj4h6ndj30sg11xdmj.jpg",
		"used": true,
		"who": "lijinshanmx"
	}, {
		"_id": "5c6385b39d21225dd7a417ce",
		"createdAt": "2019-02-13T02:49:23.946Z",
		"desc": "2019-02-13",
		"publishedAt": "2019-02-13T02:49:33.16Z",
		"source": "web",
		"type": "\u798f\u5229",
		"url": "https://ws1.sinaimg.cn/large/0065oQSqly1g04lsmmadlj31221vowz7.jpg",
		"used": true,
		"who": "lijinshanmx"
	}]
}
复制代码

上面是一个内嵌数组,需要增加两个实体类,如下: ViewResult类如下:

import 'ResultModel.dart';

class ViewResult{
  bool error;
  List<ResultModel> list;
  ViewResult(joinData){
    //获得返回的error值
    error = joinData['error'];
    list = [];
    print(joinData['results']);
    //获得"results"里的内容
    if(joinData['results'] != null){
       for(var dataItem in joinData['results']){
         list.add(new ResultModel(dataItem));
       }
    }
}
复制代码

ResultModel类如下:

class ResultModel{
   String _id;
   String createdAt;
   String desc;
   String publishedAt;
   String source;
   String type;
   String url;
   bool used;
   String who;

   ResultModel(jsonData){
     _id = jsonData['_id'];
     createdAt = jsonData['createdAt'];
     desc = jsonData['desc'];
     publishedAt = jsonData['publishedAt'];
     source = jsonData['source'];
     type = jsonData['type'];
     url = jsonData['url'];
     used = jsonData['used'];
     who = jsonData['who'];
   }
 }
复制代码

ListView的Item布局:

//需要传list 和对应下标
Widget photoWidget(List<ResultModel> resultLists,int index){
  return Card(
    child: Container(
      height: 300,
      child: Row(
        children: <Widget>[
          Expanded(
            child: ClipRRect(
              borderRadius: BorderRadius.circular(4.0),
              child: Image.network(resultLists[index].url,
                fit:BoxFit.fitWidth,
                //scale: 2.5,
              ),
            ),
          ),
        ],
      ),
    ),
  );

}
复制代码

所有代码如下:

import 'package:flutter/material.dart';
import 'dart:convert';//解码和编码JSON
import 'package:http/http.dart' as my_http;
import 'model/ViewResult.dart';
import 'model/ResultModel.dart';



//app入口
void main() {
  runApp(MyApp());
}

//用无状态控件显示
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //主题色
      theme: ThemeData(
        //设置为蓝色
          primarySwatch: Colors.red),
      //这是一个Widget对象,用来定义当前应用打开的时候,所显示的界面
      home: BigPhotoWidget(),
    );
  }
}
//主框架
class BigPhotoWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new _BigPhotoState();
  }
}

class _BigPhotoState extends State<BigPhotoWidget> {
  ViewResult viewresult;
  //具体的数据集合
  List<ResultModel> resultLists = [];
  @override
  void initState(){
    super.initState();
    getData();
  }

  getData() async{
    try{
      //因为导入http 用了as xxx方式,所以对象请求都用xxx.get方式
      //方式一
//      await my_http.get("http://gank.io/api/data/福利/10/1")
//      .then((response){
//        if(response.statusCode == 200){
//          var ViewData = json.decode(response.body);
//          viewresult = ViewResult(ViewData);
//          if(!viewresult.error){
//            //继续解析
//            for(int i = 0;i < viewresult.list.length;i++){
//              resultLists.add(viewresult.list[i]);
//            }
//            //记得调用刷新
//            setState(() {
//
//            });
//          }
//        }else{
//          print("error");
//        }
//      });
      //方式二 请求
      var response = await my_http.get("http://gank.io/api/data/福利/10/1");
      //判断状态
      if(response.statusCode == 200){
          //解析
          var ViewData = json.decode(response.body);
          viewresult = ViewResult(ViewData);
          if(!viewresult.error){
            //继续解析
            for(int i = 0;i < viewresult.list.length;i++){
              resultLists.add(viewresult.list[i]);
            }
            //记得调用刷新
            setState(() {

            });
          }
        }else{
          print("error");
        }
    }catch(e){
      print(e);

    }
  }
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      //appBar
      appBar: AppBar(
        title: Text("妹子图"),
        //标题居中
        centerTitle: true,
      ),
      body: ListView.builder(
          itemCount: resultLists.length,
          itemBuilder: (BuildContext context,int index){
            return Column(
              children: <Widget>[
                photoWidget(resultLists,index),
              ],
            );
          },
      ),
    );
  }
}


//需要传list 和对应下标
Widget photoWidget(List<ResultModel> resultLists,int index){
  return Card(
    child: Container(
      height: 300,
      child: Row(
        children: <Widget>[
          Expanded(
            child: ClipRRect(
              borderRadius: BorderRadius.circular(4.0),
              child: Image.network(resultLists[index].url,
                fit:BoxFit.fitWidth,
                //scale: 2.5,
              ),
            ),
          ),
        ],
      ),
    ),
  );

}

复制代码

上面获取数据有两种方式。

九、总结

  1. 知道Dart的简单大致执行模型,当启动一个Flutter应用时,会创建一个isolate,然后会初始化两个队列MicroTaskEvent,和初始化一个消息循环,代码执行方式和顺序取决于MicroTaskEvent队列。
  2. Futureasync都不是并行执行的。
  3. 简单的本地数据库操作。
  4. Json数据简单处理(序列化和反序列化)方式。
  5. 网络简单的请求。

作为萌新,肯定有很多技术没学到位,如有错误,欢迎指出指正,谢谢~

关注下面的标签,发现更多相似文章
评论