0%

flutter

写flutter程序

Android Studio 版本

  1. 打开Android Studio 新建 Flutter 项目
  2. 项目放在 WorkSpace/flutterWorkSpace/项目名
  3. 选择 Open iOS Simulator
  4. debug模式运行 , 修改后代码后点击闪电图标热加载

编写第一个flutter程序

  1. 打开Android Studio 新建 Flutter 项目
  2. pubspec文件管理Flutter应用程序的assets(资源,如图片、package等)。 在pubspec.yaml中,将english_words(3.1.0或更高版本)添加到依赖项列表,如下面高亮显示的行:
dependencies:
flutter:
sdk: flutter

cupertino_icons: ^0.1.0
english_words: ^3.1.0
  1. 单击右上角的 Pub get,这会将依赖包安装到您的项目。您可以在控制台中看到以下内容:
/Users/jiangangkong/flutter/bin/flutter --no-color pub get
Running "flutter pub get" in startup_01... 8.0s
Process finished with exit code 0
  1. 如遇到Error: Cannot run with sound null safety, because the following dependencies don’t support null safety: 则在该项目根目录终端上输入:flutter run --no-sound-null-safety 或者 我们可以在运行的时候添加--no-sound-null-safety。打开Android Studio,然后依次选择【Run】 –>【 Edit Configurations】 –> 【Add Additional Run args 】–> 【–no-sound-null-safety】,如下图。image-20220901215621643

设置lcon和启动画面

ios:

ios/Runner/Assets.xcassets/AppIcon.appiconset中设置lcon图片, 并修改Contents.json

ios/Runner/Assets.xcassets/LaunchImage.imageset中设置启动图片,并在同级别的Contents.json文件中配置。

路由管理 fluro

  • main.dart
import 'package:flutter/material.dart';

import 'routers/routes.dart';
import 'routers/application.dart';
import 'package:fluro/fluro.dart';

import 'pages/first_page.dart';


void main() {
FluroRouter router = FluroRouter();
Application.router = router; //一定要先写这行
Routes.configureRoutes(router);

runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: FirstPage(),
);
}
}
  • routers/application.dart
import 'package:fluro/fluro.dart';

class Application {
static late final FluroRouter router;
}
  • routers/router_handler.dart
import 'package:fluro/fluro.dart';

import '../pages/second_page.dart';
import '../pages/first_page.dart';
import '../pages/third_page.dart';
import '../pages/fourth_page.dart';
import '../pages/fifth_page.dart';
import '../pages/error_page.dart';

import 'package:flutter/material.dart';

// FirstPage
var firstPageHandler = new Handler(
handlerFunc: (BuildContext? context, Map<String, List<String>> params) {
return FirstPage();
});

// SecondPage
var secondPageHandler = new Handler(
handlerFunc: (BuildContext? context, Map<String, List<String>> params) {
return SecondPage();
});

var thirdPageHandler = new Handler(
handlerFunc: (BuildContext? context, Map<String, List<String>> params) {
return ThirdPage();
});

var fourthPageHandler = new Handler(
handlerFunc: (BuildContext? context, Map<String, List<String>> params) {
return FourthPage();
});

var fifthPageHandler = new Handler(
handlerFunc: (BuildContext? context, Map<String, List<String>> params) {
return FifthPage();
});
  • routers/routes.dart
/**
* 定义请求路径以及定义路径对应的Handler
*/

import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'router_handler.dart';
import '../pages/error_page.dart';
class Routes {

static String page1 = "/1"; //定义路由
static String page2 = "/2"; //定义路由
static String page3 = "/3"; //定义路由
static String page4 = "/4"; //定义路由
static String page5 = "/5"; //定义路由

static void configureRoutes(FluroRouter router) { //处理未匹配到路由时展示的页面
router.notFoundHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, List<String>> params) {
return ErrorPage();
});
//注册路由并指向所对应的handler(Handler对应的是界面)
router.define(page1, handler: firstPageHandler);
router.define(page2, handler: secondPageHandler);
router.define(page3, handler: thirdPageHandler);
router.define(page4, handler: fourthPageHandler);
router.define(page5, handler: fifthPageHandler);
}
}
  • pages/first_page.dart
import 'package:flutter/material.dart';
import 'package:fluro/fluro.dart';
import '../routers/application.dart';

class FirstPage extends StatefulWidget {
const FirstPage({Key? key}) : super(key: key);

@override
State<FirstPage> createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {

// 当前选中底部导航的索引
int _selectedIndex = 0;

@override
Widget build(BuildContext context) {
onGenerateRoute: Application.router.generator ;
return Scaffold(

// 顶部应用按钮
appBar: AppBar(
title: const Text('第一页'),
),

// 中部内容区
// body: Center(
// child:
// ),

// 底部导航
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.account_tree_rounded), label:'UPCOMING'),
BottomNavigationBarItem(icon: Icon(Icons.add_chart_outlined), label:'RESULTS'),
BottomNavigationBarItem(icon: Icon(Icons.add_box_sharp), label:'FIGHT PASS'),
BottomNavigationBarItem(icon: Icon(Icons.abc_sharp), label:'NEWS'),
BottomNavigationBarItem(icon: Icon(Icons.accessibility_outlined), label:'ATHLETES'),
],
type: BottomNavigationBarType.fixed, // 页面超过三个要加这句话, 不然除了正在访问的页面,其他页面导航栏全白
unselectedFontSize: 12,
selectedFontSize: 12,
iconSize: 28,
currentIndex: _selectedIndex,
fixedColor: Color.fromRGBO(221, 0, 0, 1),
onTap: _onItemTapped,
unselectedItemColor: Colors.white,
backgroundColor: Colors.black87
),
);
}

void _onItemTapped(int index){
Application.router.navigateTo(
context,
"/${index+1}",
transition: TransitionType.fadeIn // 动画
);
}
}

状态管理 provider

短时状态 与 应用状态

短时状态(有时也称 用户界面 (UI) 状态 或者 局部状态)是你可以完全包含在一个独立 widget 中的状态。

这是一个有点儿模糊的定义,这里有几个例子。

  • 一个 PageView 组件中的当前页面
  • 一个复杂动画中当前进度
  • 一个 BottomNavigationBar 中当前被选中的 tab

widget 树中其他部分不需要访问这种状态。不需要去序列化这种状态,这种状态也不会以复杂的方式改变。换句话说,不需要使用状态管理架构(例如 ScopedModel, Redux)去管理这种状态。你需要用的只是一个 StatefulWidget

如果你想在你的应用中的多个部分之间共享一个非短时的状态,并且在用户会话期间保留这个状态,我们称之为应用状态(有时也称共享状态)。

应用状态的一些例子:

  • 用户选项
  • 登录信息
  • 一个社交应用中的通知
  • 一个电商应用中的购物车
  • 一个新闻应用中的文章已读/未读状态

provider使用

dependencies:
flutter:
sdk: flutter

provider: ^6.0.0
import 'package:provider/provider.dart';

三个概念

  • ChangeNotifier【发送通知的人】

ChangeNotifier 是 Flutter SDK 中的一个简单的类。它用于向监听器发送通知。换言之,如果被定义为 ChangeNotifier,你可以订阅它的状态变化。(这和大家所熟悉的观察者模式相类似)。

class ShowAthletes extends ChangeNotifier {
void showAthletesService() async {
var options = BaseOptions(
// method: "POST",
baseUrl: "http://127.0.0.1:8000/api",
connectTimeout: 5000,
receiveTimeout: 3000,
);

Dio dio = Dio(options);

var formData = FormData.fromMap({'level': currentIndex});

Response response = await dio.post("/showAthletes", data: formData);
currentLen = response.data["len"];
notifyListeners();
}
}
  • ChangeNotifierProvider 【在mian中注册该ChangeNotifier】

ChangeNotifierProvider widget 可以向其子孙节点暴露一个 ChangeNotifier 实例。它属于 provider package。

我们已经知道了该把 ChangeNotifierProvider 放在什么位置:在需要访问它的 widget 之上。在 CartModel 里,也就意味着将它置于 MyCartMyCatalog 之上。

你肯定不愿意把 ChangeNotifierProvider 放的级别太高(因为你不希望破坏整个结构)。但是在我们这里的例子中,MyCartMyCatalog 之上只有 MyApp

void main() {
FluroRouter router = FluroRouter();
Application.router = router; //一定要先写这行
Routes.configureRoutes(router);
Provider.debugCheckInvalidValueType = null;
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ShowAthletes()),
],
child: MyApp(),
)
);
}
  • Consumer【变化的主体】
Widget buildAthleteList(){
return Consumer<ShowAthletes>(
builder: (context,sa,child){
return Expanded(
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: Icon(Icons.settings,color: Colors.blue,),
title: Text("师傅麻烦你把我送到解放碑",style: TextStyle(color: Colors.white24),),
subtitle: Text("和他挥手拜拜有缘再会",style: TextStyle(color: Colors.white24),),
);
},
itemCount: currentLen,
) ,
);
}
);
}

我们必须指定要访问的模型类型。在这个示例中,我们要访问 CartModel 那么就写上 Consumer<CartModel>

Consumer widget 唯一必须的参数就是 builder。当 ChangeNotifier 发生变化的时候会调用 builder 这个函数。(换言之,当你在模型中调用 notifyListeners() 时,所有相关的 Consumer widget 的 builder 方法都会被调用。)

builder 在被调用的时候会用到三个参数。第一个是 context。在每个 build 方法中都能找到这个参数。

builder 函数的第二个参数是 ChangeNotifier 的实例。它是我们最开始就能得到的实例。你可以通过该实例定义 UI 的内容。

第三个参数是 child,用于优化目的。如果 Consumer 下面有一个庞大的子树,当模型发生改变的时候,该子树 并不会 改变,那么你就可以仅仅创建它一次,然后通过 builder 获得该实例。

最好能把 Consumer 放在 widget 树尽量低的位置上。你总不希望 UI 上任何一点小变化就全盘重新构建 widget 吧。

  • Provider.of 【点击时触发的方法】
onPressed: () async {
setState(() {
_swt = !_swt;
});
int? selectedIndex =
await _showCustomModalBottomSheet(
context, _options);
if (selectedIndex == null) {
setState(() {
_swt = !_swt;
});
}
final sa = ShowAthletes();
Provider.of<ShowAthletes>(context,listen:false).showAthletesService();
print("自定义底部弹层:选中了第$selectedIndex个选项");
},

有的时候你不需要模型中的 数据 来改变 UI,但是你可能还是需要访问该数据。比如,ClearCart 按钮能够清空购物车的所有商品。它不需要显示购物车里的内容,只需要调用 clear() 方法。

我们可以使用 Consumer<CartModel> 来实现这个效果,不过这么实现有点浪费。因为我们让整体框架重构了一个无需重构的 widget。

所以这里我们可以使用 Provider.of,并且将 listen 设置为 false

Provider.of<CartModel>(context, listen: false).removeAll();

在 build 方法中使用上面的代码,当 notifyListeners 被调用的时候,并不会使 widget 被重构。

视频播放插件

种类

image-20220917144002341

flutter+django 前后端分离

在前后端分离的应用模式中,我们通常将后端开发的每个视图都称为一个接口,或者API,前端通过访问接口来对数据进行增删改查。

什么是RESTful API?

要弄清楚什么是RESTful API,首先要弄清楚什么是REST。REST – REpresentational State Transfer,英语的直译就是“表现层状态转移”。如果看这个概念,估计没几个人能明白是什么意思。那下面就让我来用一句人话解释一下什么是RESTful:URL定位资源,用HTTP动词(GET,POST,PUT,DELETE)描述操作。RESTful 是典型的基于HTTP的协议。

Resource:资源,即数据。
Representational:某种表现形式,比如用JSON,XML,JPEG等;
State Transfer:状态变化。通过HTTP动词实现。

所以RESTful API就是REST风格的API。 那么在什么场景下使用RESTful API呢?在当今的互联网应用的前端展示媒介很丰富。有手机、有平板电脑还有PC以及其他的展示媒介。那么这些前端接收到的用户请求统一由一个后台来处理并返回给不同的前端肯定是最科学和最经济的方式,RESTful API就是一套协议来规范多种形式的前端和同一个后台的交互方式。

RESTful API由后台也就是SERVER来提供前端来调用。前端调用API向后台发起HTTP请求,后台响应请求将处理结果反馈给前端。也就是说RESTful 是典型的基于HTTP的协议。那么RESTful API有哪些设计原则和规范呢?

资源。首先是弄清楚资源的概念。资源就是网络上的一个实体,一段文本,一张图片或者一首歌曲。资源总是要通过一种载体来反应它的内容。文本可以用TXT,也可以用HTML或者XML、图片可以用JPG格式或者PNG格式,JSON是现在最常用的资源表现形式。

统一接口。RESTful风格的数据元操CRUD(create,read,update,delete)分别对应HTTP方法:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源,这样就统一了数据操作的接口。

URI。可以用一个URI(统一资源定位符)指向资源,即每个URI都对应一个特定的资源。要获取这个资源访问它的URI就可以,因此URI就成了每一个资源的地址或识别符。一般的,每个资源至少有一个URI与之对应,最典型的URI就是URL。

无状态。所谓无状态即所有的资源都可以URI定位,而且这个定位与其他资源无关,也不会因为其他资源的变化而变化。有状态和无状态的区别,举个例子说明一下,例如要查询员工工资的步骤为第一步:登录系统。第二步:进入查询工资的页面。第三步:搜索该员工。第四步:点击姓名查看工资。这样的操作流程就是有状态的,查询工资的每一个步骤都依赖于前一个步骤,只要前置操作不成功,后续操作就无法执行。如果输入一个URL就可以得到指定员工的工资,则这种情况就是无状态的,因为获取工资不依赖于其他资源或状态,且这种情况下,员工工资是一个资源,由一个URL与之对应可以通过HTTP中的GET方法得到资源,这就是典型的RESTful风格。

说了这么多,到底RESTful长什么样子的呢?
GET:http://www.xxx.com/source/id 获取指定ID的某一类资源。例如GET:http://www.xxx.com/friends/123表示获取ID为123的会员的好友列表。如果不加id就表示获取所有会员的好友列表。

POST:http://www.xxx.com/friends/123表示为指定ID为123的会员新增好友。其他的操作类似就不举例了。

RESTful API设计准则

应该尽量将API部署在专用域名之下 https://example.org/api/
应该将API的版本号放入URL https://example.org/app/1.0/foo, 但这个是不强制的
路径又被称为终点,表示API的具体地址,每个地址代表一种资源。资源只能是名词不能是动词,而且名词往往和数据库的表名相对应。同时,利用HTTP方法(post, get, put, delete)可以分离网址中资源名称的操作。

GET /products    #返回所有的产品清单
POST /products #将产品新建到集合
GET /products/4 #将获取产品4
PATCH /products/4 #更新产品4(客户端提供改变后的完整资源)
PUT /products/4 #更新产品4(客户端提高改变的额属性)
DELETE /products/4 #删除产品4
HEAD #获取资源的元数据
OPTIONS #获取信息

API中的名词应该使用复数,无论是子资源或者所有资源

过滤信息;如果记录数量很多,服务器不可能都将它们全部返回。API需要提供参数,过滤返回结果

?limit=10  #指定返回记录的数量
?offset=10 #指定返回记录的开始位置
?page=2&per_page=100 #指定第几页,以及每页的记录数
?shortby=name&order=asc #指定返回结果按照哪个属性排序以及排序顺序
?animal_type_id=1 #指定筛选条件

RESTful API最好做到Hypermedia(即返回结果中提供链接,连向其它API方法)

服务器返回的数据格式,应该尽量采用json格式,避免使用XML

后端开发(REST接口开发)的核心任务

后端只负责返回前端需要的数据,不再渲染HTML页面,不再控制前端的效果。无论哪种前端,所需的数据基本相同,所以后端只需要开发一套逻辑对外提供数据就可以了。

在前后端分离的应用模式中,我们通常将后端开发的每个视图都称为一个接口,或者API,前端通过访问接口来对数据进行增删改查。

将请求的数据(如json格式的数据)转换成模型类对象操作数据库, 将模型类对象转换成响应的数据,比如Json格式
因此:数据类型的转换就涉及到序列化和反序列化:

在开发REST API 接口时,视图中要频繁地进行序列化和反序列化的编写。

序列化和反序列化

序列化:将程序中的一个数据结构类型转换为其它格式(字典,json, xml等),比如将django中的模型类对象转换为json字符串,这个转换过程我们成为序列化。

books_all=Books.objects.all()
book_list=[]

#序列化
for book in books_all:
book_list.append({
'id':book.id,
'title':book.title,
'author':book.author,
'publish_time':book.publish_time
})

return JsonResponse(book_list, safe=False)

反序列化:将其它格式(json,字典,XML等)转换成程序中的数据。例如将Json字符串转换成Django中的模型类对象。

json_bytes=request.body
json_str=json_bytes.decode()

#反序列化
book_dict=json.loads(json_str)
book = Books.objects.create(
title=book_dict.get('title'),
publish_time=datetime.striptime(book_dict.get('publish_time'), '%Y-%m-%d').date()
)

在这里插入图片描述

前后端分离实际操作

  • django中安装并在settings.py中注册
pip3 install djangorestframework  # 它是基于Django的,帮助我们快速开发符合restful规范的接口框架,它主要适用于前后端分离项目。
pip3 install django-cors-headers  # django-cors-headers处理跨域请求,一个为响应添加跨源资源共享(CORS)头的Django应用。这允许从其他源向Django应用程序发出浏览器内请求。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'apps.user',
'rest_framework',
'corsheaders',
] # 在settings.py中注册
  • 创建模型
from django.db import models


# Create your models here.
class User(models.Model):
""" 用户表 """
username = models.CharField(verbose_name="用户", max_length=20, null=False)
password = models.CharField(verbose_name="密码", max_length=16, null=False)
  • 在user根目录添加serializers.py文件
from rest_framework import serializers
from apps.user.models import User
from apps.user import models


class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = models.User
fields = "__all__"

image-20220911014713905

  • 生成模型
python3 manage.py makemigrations
python3 manage.py migrate

实际开发注意事项

  • 拆分appbar进components一定要实现PreferredSizeWidget借口
class MyAppBar extends StatefulWidget implements PreferredSizeWidget{
const MyAppBar({Key? key}) : super(key: key);

@override
Size get preferredSize => const Size.fromHeight(100);

@override
State<MyAppBar> createState() => _MyAppBarState();
}

class _MyAppBarState extends State<MyAppBar> {
@override
Widget build(BuildContext context) {
return AppBar(
title: const Text("UFC"),
);
}
}
  • appBar中的actions组件设置高度要用UnconstrainedBox包裹去除父样式
actions: [
UnconstrainedBox(
child :SizedBox(
height: 27,
width: 70,
child: ElevatedButton(
child: Text("live now"),
style: ElevatedButton.styleFrom(
primary: Color.fromRGBO(254, 46, 36, 1),
padding: new EdgeInsets.only(bottom: 3),
minimumSize: const Size(90, 20),
maximumSize: const Size(90, 20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5))),
onPressed: () {
print("aaa");
},
),
),
),
],
  • 运行项目后若出现The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.
cd 项目根目录/ios
pod install
  • 前端传”type”:2 后端要用”2”接收; 并且多个数据要用formData传
var formData = FormData.fromMap({
"username": usernameController.text,
'password': passwordController.text,
'type': 2,
});
Response response = await dio.post("/login", data: formData);
loginOrRegister = request.POST.get("type")  

...

if loginOrRegister == "2":
  • form.is_valid()总是返回false
//解决方法:前端提交的字段名要与数据库中字段名一致
  • TabController 不能为null
//解决方法:TabController? tabController;
  • Scaffold.of() called with a context that does not contain a Scaffold. 比如:Drawer点不出
// 解决方法: 在应用Scaffold.of(context).openDrawer();的外层套一个Builder
leading: Builder(builder: (BuildContext context){
return IconButton(
icon: Icon(Icons.menu),
tooltip: "Search",
onPressed: () {
Scaffold.of(context).openDrawer();
}
);
}),
  • Scaffold下body中有多个组件,其中之一为列表时,要用Expanded包裹

image-20220923201358714

  • flutter 前后端序列化操作
# 后端

# 返回运动员的信息
level = request.POST.get("level")
athletes = models.Athlete.objects.filter(level=level)
num = models.Athlete.objects.filter(level=level).count()

# 转化为Json对象
result = serializers.serialize('json', athletes)
res = {
'len': num,
'data': result,
}
return HttpResponse(json.dumps(res), content_type="application/json")
// 前端

class ShowAthletes extends ChangeNotifier {
void showAthletesService() async {
var options = BaseOptions(
// method: "POST",
baseUrl: "http://127.0.0.1:8000/api",
connectTimeout: 5000,
receiveTimeout: 3000,
);

Dio dio = Dio(options);

var formData = FormData.fromMap({'level': currentIndex});

Response response = await dio.post("/showAthletes", data: formData);
currentLen = response.data["len"];
notifyListeners();

// 反序列化后访问后端传来的数据
list = jsonDecode(response.data["data"]);
if(list.isNotEmpty){
for(int i = 0;i< list.length;i++){
print(list[i]["fields"]["chineseName"]);
}
}
}

*flutter持久化 [实际开发一般都是前后端分离]

sqlite和mysql

  1. 和MySQL相比,SQLite支持的数据类型较少。

  2. SQLite的可移植性较好,而MySQL较差。SQLite库大小约为250 KB,而MySQL服务器大约为600 MB。SQLite直接将信息存储在单个文件中,使其易于复制。不需要任何配置,并且可以使用最少的支持来完成该过程。

  3. MySQL有构造良好的用户管理系统,而SQLite没有。SQLite没有任何特定的用户管理功能,因此不适合多用户访问。MySQL有一个构造良好的用户管理系统,可以处理多个用户并授予不同级别的权限。SQLite适用于较小的数据库,随着数据库的增长,使用SQLite时内存需求也会变大。使用SQLite时,性能优化更加困难。相反,MySQL易于扩展,可以轻松处理更大的数据库。

  4. SQLite没有内置的身份验证机制,而MySQL有,其安全性较高。

优点和缺点 – sqlite与mysql

sqlite的优点

  • 基于文件,易于设置和使用
  • 适合基础开发和测试
  • 轻松携带
  • 使用标准SQL语法进行微小更改
  • 使用方便

SQLite的缺点

  • 缺乏用户管理和安全功能
  • 不容易扩展
  • 不适合大数据库
  • 无法定制

MySQL的优点

  • 使用方便
  • 提供了许多与数据库相关的功能
  • 良好的安全功能
  • 易于扩展,适用于大型数据库
  • 提供良好的速度和性能
  • 提供良好的用户管理和多种访问控制

MySQL的缺点

  • 需要一些技术专业知识来设置
  • 与传统SQL相比,语法略有不同

sqflite

sqflite是一款轻量级的关系型数据库,类似SQLite。在Flutter平台我们使用sqflite库来同时支持Android 和iOS。数据种类:

INTEGER : Dart type: int

REAL : Dart type: num

TEXT : Dart type: String

BLOB : Dart type: Uint8List

flutter使用sqflite

  • 准备工作
dependencies:
flutter:
sdk: flutter
fluro: ^2.0.3
dio: ^4.0.6
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
sqflite: ^2.0.3+1
import 'package:sqflite/sqflite.dart';
  • 打开数据库
var db = await openDatabase('my_db.db');
  • 关闭数据库
await db.close();
sqflite原生操作
// 使用getDatabasesPath获取位置
var databasesPath = await getDatabasesPath();
String path = join(databasesPath, 'demo.db');

// 删除数据库
await deleteDatabase(path);

// 打开数据库
Database database = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
// When creating the db, create the table
await db.execute(
'CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)');
});

// 在事务中插入一些记录
await database.transaction((txn) async {
int id1 = await txn.rawInsert(
'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');
print('inserted1: $id1');
int id2 = await txn.rawInsert(
'INSERT INTO Test(name, value, num) VALUES(?, ?, ?)',
['another name', 12345678, 3.1416]);
print('inserted2: $id2');
});

// 更新记录
int count = await database.rawUpdate(
'UPDATE Test SET name = ?, value = ? WHERE name = ?',
['updated name', '9876', 'some name']);
print('updated: $count');

// 查找全部
List<Map> list = await database.rawQuery('SELECT * FROM Test');
List<Map> expectedList = [
{'name': 'updated name', 'id': 1, 'value': 9876, 'num': 456.789},
{'name': 'another name', 'id': 2, 'value': 12345678, 'num': 3.1416}
];
print(list);
print(expectedList);
assert(const DeepCollectionEquality().equals(list, expectedList));

// 记录条数
count = Sqflite
.firstIntValue(await database.rawQuery('SELECT COUNT(*) FROM Test'));
assert(count == 2);

// 删除记录
count = await database
.rawDelete('DELETE FROM Test WHERE name = ?', ['another name']);
assert(count == 1);

// 关闭数据库
await database.close();
SQL helpers操作
final String tableTodo = 'todo';
final String columnId = '_id';
final String columnTitle = 'title';
final String columnDone = 'done';

// 实体类
class Todo {
int id;
String title;
bool done;

Map<String, Object?> toMap() {
var map = <String, Object?>{
columnTitle: title,
columnDone: done == true ? 1 : 0
};
if (id != null) {
map[columnId] = id;
}
return map;
}

Todo();

Todo.fromMap(Map<String, Object?> map) {
id = map[columnId];
title = map[columnTitle];
done = map[columnDone] == 1;
}
}

class TodoProvider {
Database db;

// 打开数据库并创建表
Future open(String path) async {
db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db.execute('''
create table $tableTodo (
$columnId integer primary key autoincrement,
$columnTitle text not null,
$columnDone integer not null)
''');
});
}

// 插入记录
Future<Todo> insert(Todo todo) async {
todo.id = await db.insert(tableTodo, todo.toMap());
return todo;
}

// 根据id查询记录
Future<Todo> getTodo(int id) async {
List<Map> maps = await db.query(tableTodo,
columns: [columnId, columnDone, columnTitle],
where: '$columnId = ?',
whereArgs: [id]);
if (maps.length > 0) {
return Todo.fromMap(maps.first);
}
return null;
}

// 根据id删除记录
Future<int> delete(int id) async {
return await db.delete(tableTodo, where: '$columnId = ?', whereArgs: [id]);
}

// 给一个实体类,根据它的id更新记录
Future<int> update(Todo todo) async {
return await db.update(tableTodo, todo.toMap(),
where: '$columnId = ?', whereArgs: [todo.id]);
}

// 关闭数据库
Future close() async => db.close();
}
事务

不要使用数据库,而只使用事务中的Transaction对象来访问数据库

await database.transaction((txn) async {
// Ok
await txn.execute('CREATE TABLE Test1 (id INTEGER PRIMARY KEY)');

// DON'T use the database object in a transaction
// this will deadlock!
await database.execute('CREATE TABLE Test2 (id INTEGER PRIMARY KEY)');
});
批量操作
batch = db.batch();
batch.insert('Test', {'name': 'item'});
batch.update('Test', {'name': 'new_item'}, where: 'name = ?', whereArgs: ['item']);
batch.delete('Test', where: 'name = ?', whereArgs: ['item']);
results = await batch.commit();

警告,在事务期间,批处理在事务提交之前不会被提交

await database.transaction((txn) async {
var batch = txn.batch();

// ...

// commit but the actual commit will happen when the transaction is committed
// however the data is available in this transaction
await batch.commit();

// ...
});

默认情况下,批处理一旦遇到错误就会停止(通常会恢复未提交的更改)。你可以忽略错误,这样即使有一个操作失败,每个成功的操作都会运行并提交:

await batch.commit(continueOnError: true);