01 flutter3 环境搭建
最新版的flutter在进行环境搭建的时候需要额外安装Visual Studio,以对windows桌面版开发功能进行支持。可以在命令行中输入以下配置跳过这一步骤:
flutter config --no-enable-windows-desktop
02 flutter简要介绍
在Flutter中,大多数东西都是widget,包括对齐(alignment)、填充(padding)和布局(layout)
widget的主要工作是提供一个build()方法来描述如何根据其他较低级别的widget来显示自己。
使用外部包的方法:在pubspec.yaml中,将english_words(添加到依赖项列表,如下面高亮显示的
dependencies:
flutter:
sdk: flutter
english_words: ^3.1.5
注意格式的对齐,然后点击上方Packages get,这会将依赖包安装到项目。然后再引入即可。
Flutter 从上到下可以分为三层:框架层、引擎层和嵌入层
Flutter 框架的的处理流程是这样的:
根据 Widget 树生成一个 Element 树,Element 树中的节点都继承自 Element 类。
根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自RenderObject 类。
根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自 Layer 类。
真正的布局和渲染逻辑在 Render 树中,Element 是 Widget 和 RenderObject 的粘合剂,可以理解为一个中间代理。
三棵树中,Widget 和 Element 是一一对应的,但并不和 RenderObject 一一对应。
json实体类自动生成工具:https://caijinglong.github.io/json2dart/index_ch.html
终端命令:
flutter packages pub run build_runner build
Flutter
布局基础——Stack
层叠布局
层叠布局适用于子视图叠放一起,且位置能够相对于父视图边界确认的情况。
比如,可用于图片上加文字,按钮上加渐变阴影等等。
Stack Widget
的子视图要么是positioned
,要么是non-positioned
。Positioned
子视图是指使用Positioned
的widget
包括起来的子视图,通过设置相对于Stack
的top
、bottom
、left
、right
属性来确认自身位置,其中至少要有一个不为空。
Stack Widget
的大小取决于所有non-positioned
的子视图。non-positioned
的子视图的位置根据alignment
属性确定,(当alignment
为left-to-right
时,子视图默认从左上角开始;当aligment
为right-to-left
时,子视图从右上角开始;)。
Flutter中使用Stack
和Positioned
这两个组件来配合实现绝对定位。Stack
允许子组件堆叠,而Positioned
用于根据Stack
的四个角来确定子组件的位置。
Dart
——数据类型:键值对Map
map
是一个无序的 key-value
(键值对)集合,其中键和值都可以是任何类型的对象。每个 键 只能出现一次但是 值 可以重复出现多次。 Dart
中 Map
提供了 Map
字面量以及 Map
类型两种形式的 Map
。
使用 Map
的构造器创建 Map
var gifts = Map<String, String>();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';
向现有的 Map
中添加键值对
var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds';
从一个 Map
中获取一个值
var gifts = {'first': 'partridge'};
assert(gifts['first'] == 'partridge');
var nobleGases = {54: 'xenon'};
// Retrieve a value with a key.
assert(nobleGases[54] == 'xenon');
// Check whether a map contains a key.
assert(nobleGases.containsKey(54));
// Remove a key and its value.
nobleGases.remove(54);
assert(!nobleGases.containsKey(54));
可以从一个 map 中检索出所有的 key 或所有的 value:
var hawaiianBeaches = {
'Oahu': ['Waikiki', 'Kailua', 'Waimanalo'],
'Big Island': ['Wailea Bay', 'Pololu Beach'],
'Kauai': ['Hanalei', 'Poipu']
};
// Get all the keys as an unordered collection
// (an Iterable).
var keys = hawaiianBeaches.keys;
assert(keys.length == 3);
assert(Set.from(keys).contains('Oahu'));
// Get all the values as an unordered collection
// (an Iterable of Lists).
var values = hawaiianBeaches.values;
assert(values.length == 3);
assert(values.any((v) => v.contains('Waikiki')));
使用 .length
可以获取 Map
中键值对的数量
在一个 Map
字面量前添加 const
关键字可以创建一个 Map
编译时常量
Dart
——数据类型:泛型
一个只能包含 String
类型的数组,可以将该数组声明为 List
(读作“字符串类型的 list
”)
在
flutter
项目中使用Hive
进行数据本地存储
hive
是一个高性能和轻量级的nosql
数据库,使用键值对形式进行存储。
Hive
的四个简单步骤:Setup Hive
dependencies:
flutter:
sdk: flutter
hive: ^2.0.4
hive_flutter: ^1.1.0
dev_dependencies:
flutter_test:
sdk: flutter
hive_generator: ^1.0.0
build_runner: ^1.12.2
// 1.
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
void main() async {
// 2.
WidgetsFlutterBinding.ensureInitialized();
// 初始化hive
await Hive.initFlutter();
runApp(MyApp());
}
Create Hive Model
将想要进行本地存储的 model classes
转化为 hive models
.
class Transaction {
late String name;
late DateTime createdData;
late bool isExpense = true;
late double amount;
}
如果你想在你的配置单元数据库存储中进行存储和加载你的模型类,那么需要遵循三个步骤:
// 1.
import 'package:hive/hive.dart';
// 这个文件是稍后生成的模拟类适配器
part 'transaction.g.dart';
// 2. 注释模型类,并提供所有模型类之间的唯一数字
@HiveType(typeId: 0)
// 从单元对象扩展你的模型类,从而使用HiveObject里面的所有方法
class Transaction extends HiveObject {
// 3. 注释模型类中要保留在hive数据库中的所有字段
@HiveField(0)
late String name;
@HiveField(1)
late DateTime createdData;
@HiveField(2)
late bool isExpense = true;
@HiveField(3)
late double amount;
}
Model Adapter
)hive system
)中注册这个模型类适配器 main.dart:import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
// 1.
import './model/transaction.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 初始化hive
await Hive.initFlutter();
// 2.
Hive.registerAdapter(TransactionAdapter());
runApp(MyApp());
}
Use CRUD Operations
配置增删改查操作
先来了解hive存储数据的原理:
使用我们的box
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 初始化hive
await Hive.initFlutter();
Hive.registerAdapter(TransactionAdapter());
// 1.打开指定box
await Hive.openBox<Transaction>("transactions");
runApp(MyApp());
}
class _TransactionPageState extends State<TransactionPage> {
final List<Transaction> transactions = [];
// 定义hive相关方法
@override
void dispose() {
// 1. 关闭所有的 hive boxes
Hive.close();
// 2. 关闭名为 transactions 的盒子
Hive.box("transactions").close();
super.dispose();
}
Connect UI To Hive
使UI监听到数据的修改并重新渲染页面
使用一些监听的方法:ValueListenableBuilder()
TextEditingController()
等。
(JohannesMilke /hive_database_example)
import 'package:flutter/material.dart';
// import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'model/transaction.dart';
import 'pages/transaction_page.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 初始化hive
await Hive.initFlutter();
Hive.registerAdapter(TransactionAdapter());
await Hive.openBox<Transaction>("transactions");
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
static const String title = "Hive Expense App";
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) => MaterialApp(
debugShowCheckedModeBanner: false,
title: title,
theme: ThemeData(primarySwatch: Colors.indigo),
home: const TransactionPage(),
);
}
import 'package:hive/hive.dart';
part 'transaction.g.dart';
@HiveType(typeId: 0)
class Transaction extends HiveObject {
@HiveField(0)
late String name;
@HiveField(1)
late DateTime createdData;
@HiveField(2)
late bool isExpense = true;
@HiveField(3)
late double amount;
}
import 'package:flutter/material.dart';
import 'package:myapp/model/transaction.dart';
// import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
// 转化时间格式的工具
import 'package:intl/intl.dart';
import 'package:myapp/Widget/transaction_dialog.dart';
class TransactionPage extends StatefulWidget {
const TransactionPage({Key? key}) : super(key: key);
@override
_TransactionPageState createState() => _TransactionPageState();
}
class _TransactionPageState extends State<TransactionPage> {
// 定义hive相关方法
@override
// dispose方法(生命周期 ):在State对象从树中被永久移除时调用
void dispose() {
// 关闭所有的 hive boxes
Hive.close();
// 关闭名为 transactions 的盒子
// Hive.box("transactions").close();
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text("Hive Expense Tracker"),
centerTitle: true,
),
// 在这里设置监听
body: ValueListenableBuilder<Box<Transaction>>(
valueListenable: Boxes.getTransactions().listenable(),
builder: (context, box, _) {
// 得到的值要进行类型转换
final transactions = box.values.toList().cast<Transaction>();
// 返回一个构建内容的组件
return buildContent(transactions);
},
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => showDialog(
context: context,
builder: (context) => const TransactionDialog(
onClickedDone: addTransaction,
))),
);
}
// 传入 Transaction 类型的 transactions
Widget buildContent(List<Transaction> transactions) {
// 内容如果为空显示占位符
if (transactions.isEmpty) {
return const Center(
child: Text(
"No expenses yet!",
style: TextStyle(fontSize: 24),
),
);
} else {
// 调用math fold方法,从初始值0开始,在过程中发生改变
final netExpense = transactions.fold<double>(
0,
(previousValue, transaction) => transaction.isExpense
? previousValue - transaction.amount
: previousValue + transaction.amount,
);
final newExpenseString = "\$${netExpense.toStringAsFixed(2)}"; // 四舍五入
final color = netExpense > 0 ? Colors.green : Colors.red;
return Column(
children: [
const SizedBox(height: 24),
Text(
"Net Expense: $newExpenseString",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
color: color,
),
),
const SizedBox(height: 24),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: transactions.length,
itemBuilder: (BuildContext context, int index) {
final transaction = transactions[index];
//传递两个参数
return buildTransaction(context, transaction);
}),
),
],
);
}
}
Widget buildTransaction(
BuildContext context,
Transaction transaction,
) {
final color = transaction.isExpense ? Colors.red : Colors.green;
final date = DateFormat.yMMMd().format(transaction.createdData);
final amount = "\$" + transaction.amount.toStringAsFixed(2);
return Card(
color: Colors.white,
// 可以展开的标题控件
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
title: Text(
transaction.name,
maxLines: 2,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
subtitle: Text(date),
trailing: Text(
amount,
style:
TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 16),
),
children: [
buildButtons(context, transaction),
],
),
);
}
Widget buildButtons(BuildContext context, Transaction transaction) => Row(
children: [
Expanded(
child: TextButton.icon(
label: const Text("Edit"),
icon: const Icon(Icons.edit),
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder:(context) => TransactionDialog(
transaction:transaction,
onClickedDone:(name, amount, isExpense) =>
editTransaction(transaction,name,amount,isExpense),
),),
),),),
Expanded(
child: TextButton.icon(
label: const Text("Delete"),
icon: const Icon(Icons.delete),
onPressed: () => deleteTransaction(transaction),
)
),
],
);
// 添加的方法
Future addTransaction(String name, double amount, bool isExpense) async {
// 创建一个新的 Transaction 对象
final transaction = Transaction()
..name = name
..createdData = DateTime.now()
..amount = amount
..isExpense = isExpense;
// 首先获取box 创建一个新类:Boxes
final box = Boxes.getTransactions();
// 调用添加方法
box.add(transaction);
// box.put("mykey", transaction);
}
// 更新的方法
void editTransaction(
Transaction transaction,
String name,
double amount,
bool isExpense,
) {
transaction.name = name;
transaction.amount = amount;
transaction.isExpense = isExpense;
// 调用保存的方法
transaction.save();
}
// 删除的方法
void deleteTransaction(Transaction transaction) {
// 调用删除的方法
transaction.delete();
}
// 监听对象
class Boxes {
static Box<Transaction> getTransactions() =>
// 调用hivebox来访问box
Hive.box<Transaction>("transactions");
}
import 'package:flutter/material.dart';
import 'package:myapp/model/transaction.dart';
class TransactionDialog extends StatefulWidget {
final Transaction? transaction;
final Function(String name, double amount, bool isExpense) onClickedDone;
const TransactionDialog(
{Key? key, this.transaction, required this.onClickedDone})
: super(key: key);
@override
_TransactionDialogState createState() => _TransactionDialogState();
}
class _TransactionDialogState extends State<TransactionDialog> {
// 局部更新:GlobalKey 可以获取到对应的 Widget 的 State 对象
final formKey = GlobalKey<FormState>();
// TextEditingController:TextField控制器,
// 用来操作TextField,为绑定的输入框预设内容;获取输入的内容;监听文字输入变化与焦点变化
final nameController = TextEditingController();
final amountController = TextEditingController();
bool isExpense = true;
@override
// 页面初始化
void initState() {
super.initState();
if (widget.transaction != null) {
final transaction = widget.transaction!; // 做后缀的!会让左侧的表达式转成对应的非空类型
nameController.text = transaction.name;
amountController.text = transaction.amount.toString();
isExpense = transaction.isExpense;
}
}
@override
// state被销毁
void dispose() {
nameController.dispose();
amountController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isEditing = widget.transaction != null;
final title = isEditing ? "Edit Transaction" : "Add Transaction";
return AlertDialog(
title: Text(title),
content: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 8,
),
buildName(),
const SizedBox(
height: 8,
),
buildAmount(),
const SizedBox(
height: 8,
),
buildRadioButtons(),
],
),
),
),
actions: [
buildCancelButton(context),
buildAddButton(context, isEditing: isEditing),
],
);
}
Widget buildName() => TextFormField(
controller: nameController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: "Enter Name",
),
validator: (name) =>
name != null && name.isEmpty ? "Enter a name" : null,
);
Widget buildAmount() => TextFormField(
controller: amountController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: "Enter Amount",
),
keyboardType: TextInputType.number,
validator: (amount) => amount != null && double.tryParse(amount) == null
? "Enter a valid number"
: null,
);
Widget buildRadioButtons() => Column(
children: [
RadioListTile<bool>(
title: const Text("Expense"),
value: true,
groupValue: isExpense,
onChanged: (value) => setState(() {
isExpense = value!;
}),
),
RadioListTile<bool>(
title: const Text("Income"),
value: false,
groupValue: isExpense,
onChanged: (value) => setState(() {
isExpense = value!;
}),
),
],
);
Widget buildCancelButton(BuildContext context) => TextButton(
child: const Text("Cancel"),
onPressed: () => Navigator.of(context).pop(),
);
Widget buildAddButton(BuildContext context, {required bool isEditing}) {
final text = isEditing ? "Save" : "Add";
return TextButton(
child: Text(text),
onPressed: () async {
final isValid = formKey.currentState!.validate();
if (isValid) {
final name = nameController.text;
final amount = double.tryParse(amountController.text) ??
0; //如果等号后面的值为 null 则赋值为0
widget.onClickedDone(name, amount, isExpense);
Navigator.of(context).pop();
}
},
);
}
}
dependencies:
flutter:
sdk: flutter
hive: ^2.0.4
hive_flutter: ^1.1.0
dev_dependencies:
flutter_test:
sdk: flutter
hive_generator: ^1.0.0
build_runner: ^1.12.2
intl: ^0.17.0
从本地&网络加载
json
数据
[
{
"username": "Emma Spoon",
"email": "[email protected]",
"urlAvatar": "https://images.unsplash.com/photo-1604426633861-11b2faead63c?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=700&q=80"
},
...
]
class User {
final String username;
final String email;
final String urlAvatar;
// 创建构造函数并将所有字段放在里面
const User({
required this.username,
required this.email,
required this.urlAvatar,
});
// 创建 formJSON 方法:此方法返回一个用户,获取JSON的所有字段
static User fromJSon(json) => User(
username: json["username"],
email: json["email"],
urlAvatar: json["urlAvatar"]);
}
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_application_demo/model/user.dart';
// import 'package:sky_engine/_http/http.dart' as http;
class UsersApi {
// 获取来自 user json文件的列表
static Future<List<User>> getUsersLocally(BuildContext context) async {
// 加载json文件
final AssetBundle = DefaultAssetBundle.of(context);
// 将数据作为一个字符串加载
final data = await AssetBundle.loadString("assets/users.json");
// 同时把它转化为json对象
final body = json.decode(data);
// 将json中的数据映射到用户对象上
return body.map<User>(
// 传入用户对象,从定义的json方法中创建
User.fromJSon
).toList();// 获取用户列表
}
}
import 'package:flutter/material.dart';
import 'package:flutter_application_demo/api/users_api.dart';
import 'package:flutter_application_demo/model/user.dart';
class UserLocalPage extends StatelessWidget {
const UserLocalPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) => Scaffold(
// FutureBuilder 时一个将异步操作和异步UI更新结合在一起的类,
// 通过它我们可以将网络请求,数据库读取等的结果更新在页面上
body: FutureBuilder<List<User>>(
// 调用 UsersApi 获取本地用户信息
/* future对象表示此构建器当前连接的异步计算 */
future: UsersApi.getUsersLocally(context), // 传入一个上下文 获取json对象列表
// 得到了数据之后将其构建为小组件
/* builder : AsyncWidgetBuilder 类型的回调函数,是一个基于异步交互构建widget的函数
它接受两个参数,BuildContext context 与 AsyncSnapshot snapshot
它返回一个 widget */
builder: (context, snapshot) {
// 获取到 Users 对象
/* AsyncSnapshot 包含异步计算的信息
它具有以下属性:data —— 异步计算接收的最新数据
error —— 异步计算接收的最新错误对象
connectionState —— 美剧connectionState的值,表示与异步计算的连接状态,有四个值
none,waiting,active,done
*/
final users = snapshot.data;
switch (snapshot.connectionState) {
case ConnectionState.waiting:
// 圆形进度指示器
return const Center(child: CircularProgressIndicator());
// 加载成功的情况
default:
if(snapshot.hasError) {
return const Center(child: Text("Some error occurred!"));
} else{
return buildUsers(users!);
}
}
},
),
);
// 创建构造用户的方法
Widget buildUsers(List<User>users) => ListView.builder(
physics: const BouncingScrollPhysics(),
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(user.urlAvatar),
),
title: Text(user.username),
subtitle: Text(user.email),
);
},
);
}