安装
java版本控制(jvms)
我们可能需要多个java版本,主要是为了兼容Gradle版本。
- 下载,解压到想要的路径
- 管理员身份打开cmd,cd到在jvms.exe所在的目录下,执行
jvms init
jvms.exe rls
可列出java版本,如果失败,打开https://site.ip138.com/raw.Githubusercontent.com/
查看ip,在C:\Windows\System32\drivers\etc\hosts
末尾添加一行[ip] raw.githubusercontent.com
- 安装
jvms install [version]
jvms.exe ls
列出已安装JDK版本jvms.exe switch [version]
切换版本java -version
jvms.exe remove [version]
删除某个版本
更多命令
// 创建新项目
fvm flutter create
// 更新项目依赖
fvm flutter pub get
// 清除缓存
fvm flutter clean
Android Studio
安装配置Android Studio
Android SDK Tools:
- path:
C:\Users\[username]\AppData\Local\Android\Sdk\tools
Android SDK Platform-Tools:
- path:
C:\Users\[username]\AppData\Local\Android\Sdk\platform-tools
Android SDK
Tools -> SDK Manager -> 编辑Android SDK Location(默认即可)
- SDK Platforms:推荐Android 7.0及以上
- SDK Tools
- 下载安装 -> Intel x86 Emulator Accelerator (HAXM installer)-Deprecated(模拟器支持)
- Android SDK Command-line Tools
flutter版本控制(fvm)
- 安装 fvm,命令行运行以下命令:
choco install fvm
- 查看当前有哪些版本可用
fvm releases
- 安装指定版本
// 有可能缺失文件,建议手动下载
fvm install [version]
- 列出所有已安装的 Flutter SDK 版本。
fvm list
- VS Code配置
在项目中创建一个.vscode文件夹,然后创建一个名为settings.json的文件并添加:
{
"dart.flutterSdkPath": ".fvm/flutter_sdk",
// Remove .fvm files from search
"search.exclude": {
"**/.fvm": true
},
// Remove from file watching
"files.watcherExclude": {
"**/.fvm": true
}
}
- 切换版本
- 全局切换:
fvm global [version]
- 项目中切换(每次都要):项目目录下,终端运行
fvm use [version]
,重启vscode
运行上述命令后,项目中创建了一个名为.fvm 的文件夹,文件夹中有 flutter SDK,如果不希望提交此文件夹,在.gitignore文件中添加.fvm/flutter_sdk
- 删除某个版本
fvm remove [version]
直接安装
- 安装 JDK
- 安装flutter(windows)
set PUB_HOSTED_URL="https://mirrors.tuna.tsinghua.edu.cn/dart-pub" #配置国内镜像
set FLUTTER_STORAGE_BASE_URL="https://mirrors.tuna.tsinghua.edu.cn/flutter"
- 配置环境变量
Dart: 无需配置,flutter现在自带dart。
Flutter:
- path:
C:\flutter\bin
Java:
JAVA_HOME
:C:\Program Files\Java\jdk-1.8
path:
%JAVA_HOME%\bin
path:
%JAVA_HOME%\jre\bin
(如果是jdk-17则不需要配置jre)删除自动配置的环境变量path:
C:\Program Files\Common Files\Oracle\Java\javapath
C:\Program Files (x86)\Common Files\Oracle\Java\javapath
不推荐设置系统环境变量classpath
,始终建议通过-cp
命令传入,JVM默认的classpath
为.
,即当前目录。
创建 Flutter 项目
构建
构建项目工具是必学内容,也是项目容易报错的部分,所以放在前面。
Gradle是Android官方工具,比Maven更灵活高效,它具有高度可定制性,以适应不同的项目。
Gradle
注意不同版本的Gradle存在差异
- 一般情况下Android项目使用Android Gradle Plugin(AGP)的版本,新项目请检查AGP对应gradle版本
- flutter SDK有它自己对应的gradle版本
- clone的java项目下载相应gradle到项目目录,在
Settings -> Build Tools -> Distribution:Local installation
使用。
更新AGP:Tools -> AGP upgrade assistant
更改项目/模块gradle版本:File -> Project Structure -> Project/Modules
参考:文档、示例:https://docs.gradle.org/[version]/samples/index.html#java
基础
- 项目 build.gradle:配置项目整体属性,比如指定的代码仓库、依赖
buildscript {
repositories { // gradle脚本执行需要的依赖
google() // 引用google上的开源项目
jcenter() // 引用 jcenter上的开源项目
}
dependencies { // 依赖的jar包
classpath 'com.android.tools.build:gradle:3.0.0'
}
}
allprojects { // 项目本身需要的依赖
repositories {
google()
jcenter()
}
}
task clean(type: Delete) { // 执行task任务:删除根目录中的build目录
delete rootProject.buildDir
}
- 模块 build.gradle:配置当前Module的编译参数
// 使用插件
apply plugin: 'com.android.application'
android {
compileSdk 34 // 设置编译时用的Android版本
defaultConfig {
applicationId "com.example.myapplication" // 项目的包名(子模块不能指定)
minSdkVersion 15 // 最低兼容的版本
targetSdk 34 // 目标版本
versionCode 1 // 版本号
versionName "1.0" // 版本名称
// 使用AndroidJUnitRunner进行单元测试
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release { // 生产环境
buildConfigField("boolean", "LOG_DEBUG", "false") // 配置Log日志
buildConfigField("String", "URL_PERFIX", ""https://release.cn/"") // 配置URL前缀
minifyEnabled false // 是否对代码进行混淆
//指定混淆的规则文件
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release // 设置签名信息
pseudoLocalesEnabled false // 是否在APK中生成伪语言环境,帮助国际化
zipAlignEnabled true // 是否对APK包进行ZIP对齐优化
applicationIdSuffix 'test' // 在applicationId 中添加了一个后缀,一般使用的不多
versionNameSuffix 'test' // 在applicationId 中添加了一个后缀,一般使用的不多
}
debug { // 测试环境
buildConfigField("boolean", "LOG_DEBUG", "true") // 配置Log日志
buildConfigField("String", "URL_PERFIX", ""https://test.com/"") // 配置URL前缀
minifyEnabled false //是否对代码进行混淆
//指定混淆的规则文件
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug // 设置签名信息
debuggable false // 是否支持断点调试
jniDebuggable false // 是否可以调试NDK代码
renderscriptDebuggable false // 是否开启渲染脚本
zipAlignEnabled true // 是否对APK包执行ZIP对齐优化
pseudoLocalesEnabled false // 是否在APK中生成伪语言环境,帮助国际化
applicationIdSuffix 'test' // 在applicationId 中添加了一个后缀,一般使用的不多
versionNameSuffix 'test' // 在applicationId 中添加了一个后缀,一般使用的不多
}
}
}
dependencies { //项目的依赖关系
👉// 本地jar包依赖:新建 Project\app\libs 文件夹,粘贴 jar
implementation fileTree(include: ['*.jar'], dir: 'libs')
testImplementation 'junit:junit:4.12' // 声明测试用例库
implementation 'com.android.support:appcompat-v7:26.1.0' // 远程依赖
implementation project(':library') // 项目本地的Library模块
}
其他配置
android {
signingConfigs { // 自动化打包配置
release { // 线上环境
keyAlias 'test'
keyPassword '123456'
storeFile file('test.keystore')
storePassword '123456'
}
debug { // 开发环境
keyAlias 'test'
keyPassword '123456'
storeFile file('test.keystore')
storePassword '123456'
}
}
sourceSets { // 目录指向配置
main {
jniLibs.srcDirs = ['libs'] // 指定lib库目录
}
}
packagingOptions{
// 当有重复文件时 ,使用第一个匹配的文件打包进apk
pickFirsts = ['META-INF/LICENSE']
// 当出现重复文件时 合并重复的文件打包进apk
merge 'META-INF/LICENSE'
// 同时使用butterknife、dagger2框架处理 (常用)
exclude 'META-INF/services/javax.annotation.processing.Processor'
}
productFlavors {
wandoujia { // 豌豆荚渠道包配置
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wandoujia"]
}
xiaomi {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "xiaomi"]
applicationId "com.wiky.gradle.xiaomi" // 配置包名
}
_360 {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "_360"]
}
//...
}
lintOptions { // 关闭检查lint(有错误会停止build)
abortOnError false // 即使报错也不会停止打包
checkReleaseBuilds false // 打包release版本的时候进行检测
}
buildFeatures { // true表示生成build/app/generated/source/buildConfig/release/com/example/app/BuildConfig.java,不需要手动维护版本号、渠道等常量
buildConfig = false
}
}
- gradle-wrapper-properties:配置 Gradle Wrapper
- gradle-properties:配置 Gradle编译参数,详见文档
- setting.gradle:配置 Gradle 的多项目管理
- local.properties:存放 Android 项目的私有属性配置,如 SDK 路径
- multiDexKeep.pro、proguard-rules.pro:可选的混淆文件,用于配置放置在主 Dex 的类、声明避免混淆的类
java插件
简介
- Android App:APK文件,新建时选择Empty Views Activity
- Android Library:ARR文件,在目录结构上与Android App相同,包含构建APP所需的一切。当你需要构建不同的APK时,具有通用的模块(如账户管理),可以将library添加为每个APP模块的依赖项
- Java or Kotlin Library(插件):JAR文件,打包可重用的代码,不含资源文件,如res中的图片
使用
- app/build.gradle使用library插件(项目下的Android Library已默认使用library插件)
// 更改为library
plugins {
// id 'com.android.application'
id 'com.android.library'
}
// 注释掉applicationId
android {
defaultConfig {
// applicationId "com.example.myapp"
}
}
AndroidManifest注释掉application配置
处理本地aar
- 在项目根目录中创建一个新文件夹,例如spotify-app-remote,把spotify-app-remote.arr放入,并创建新的build.gradle,添加
configurations.maybeCreate("default")
artifacts.add("default", file('spotify-app-remote-release-0.7.1.aar'))
- settings.gradle 添加文件夹
include ':spotify-app-remote'
- build.gradle 添加文件夹(使用此arr的模块如app)
dependencies{
api project(':spotify-app-remote')
}
- 生成jar\arr
Android Studio 打开右侧的 Gradle,选择需要打包的module —> Tasks —> build, 双击 assemble
生成jar\arr到Project\build\library\libs或outputs\aar,移到Project\app\libs
在模块gradle添加
implementation fileTree(dir: 'libs', include: ['*.aar'])
implementation fileTree(dir: 'libs', include: ['*.jar'])
自定义插件的三种方式
- build script:在
build.gradle
脚本中直接编写,只能在本文件内使用 - buildSrc项目:新建一个名为
buildSrc
的Module,Gradle会自动编译和测试,只能在本项目中使用 - 独立项目:在独立的项目中编写插件,发布到本地或者远程jcenter、maven仓库供其他项目使用
独立项目打包jar到maven:
在项目中新建File -> New -> New Module -> Java or Kotlin Library
修改build.gradle:
// Gradle插件
apply plugin: 'groovy'
apply plugin: 'maven'
apply plugin: 'java-gradle-plugin'
// 依赖
dependencies {
implementation gradleApi() //Groovy DSL
implementation localGroovy() //Gradle DSL
}
// 仓库
repositories {
mavenCentral()
}
// 配置插件id和映射类(生成resources文件夹储存)
gradlePlugin {
plugins {
greeting {
// 插件id
id = 'CustomPlugin'
// 插件实现类
implementationClass = 'com.group.myplugin.CustomPlugin'
}
}
}
def group='com.group.myplugin'
def artifactId='myplugin'
def version='1.0.0'
// 指定本地maven的路径,在插件目录下
def uploadRepo = '../myplugin'
// 打包到本地maven仓库
uploadArchives {
repositories {
mavenDeployer {
pom.groupId = group
pom.artifactId = artifactId
pom.version = version
repository(url: uri(uploadRepo))
}
}
}
- 新建CustomPlugin.groovy文件写插件:实现Plugin接口
package com.group.myplugin
import org.gradle.api.Plugin
import org.gradle.api.Project
class CustomPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
println("Hello gradle plugin")
}
}
- 将插件发布到本地 Maven 仓库的2种方式:
执行
./gradlew uploadArchives
命令可视化界面的 uploadArchives Task 点击发布
故障排除
配置
- 配置build.gradle(project:android)
allprojects {
repositories {
google()
jcenter()
maven { url "https://storage.googleapis.com/download.flutter.io" }
}
}
更新
- flutter
flutter upgrade
flutter doctor
报错
查看错误信息
控制台Terminal:./gradlew assembleDebug --info
常见错误
- 更新Android Studio
- 重新建项目
- \android\app\build.gradle
compileSdkVersion 33
minSdkVersion 21
其他错误
- Attribute application@label value=(Dormitory) from AndroidManifest.xml
解决:进入\android\app\src\main\AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" // 添加此行
package="com.example.model_dev">
<application
tools:replace="android:label" // 添加此行
android:label="model_dev"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
- SDK XML versions up to 3 but an SDK XML file of version 4 was encountered
解决:local.properties添加转义符C\:\\Users
- Gradle threw an error while downloading artifacts from the network
解决:Gradle损坏,进入 C:\Users\username 删除 .gradle 文件夹
- Execution failed for task:generateDebugRFile
解决:Android Studio -> Analyze -> Code Inspect
dart
变量
# 创建一个变量并将其初始化
var name = 'Bob';
# 指定类型
String name = 'Bob';
# 如果对象(name)不局限于单一的类型(例如String),可以将其指定为 Object(或 dynamic)类型
Object name = 'Bob';
? 可空类型
int? a=123; // int? 表示a是一个可空类型
int? a; // 未初始化变量的初始值为null
! 类型断言
a! // a不为空
- late 延迟初始化:字段被第一次访问时延迟运行,而不是在实例化后立即运行。
class Weather {
late int _temperature = _readThermometer();
}
- final 已经实例化的final对象不能指向另一个不同的对象,但其字段可以更改。
final name = 'Bob';
final String nickname = 'Bobby';
- const 编译时常量:一经定义就会在编译期间对其进行初始化,const对象及其字段无法更改。
类型
常规类型
注意首字母大小写
int 整数值
double 浮点数字
String 字符串
bool 布尔类型
List 列表类型
Map 字典型
List
属性:- length 长度
- reversed 翻转
- isEmpty 是否为空
- isNotEmpty 是否不为空
方法:
- add() 增加
- addAll() 拼接数组
- indexOf() 查找 传入具体值
- remove() 删除 传入具体值
- removeAt() 删除 传入索引值
- fillRange() 修改
- insert(index,value); 指定位置插入
- insertAll(index,list) 指定位置插入List
- toList() 其他类型转换成List
- join() List转换成字符串
- split() 字符串转化成List
- forEach()
- map()
示例:List<Widget>.from(MyMap.keys.map((item) => SomeWidget(item)).toList());
map()可以传入List或Map的每项数据,toList()转换成List,List.from接收Iterable([Map.keys]、[List] 和 [Set] 都是 Iterable
),返回widget List。 - where()
- any()
- every()
Map
属性:- keys 获取所有的key值
- values 获取所有的value值
- isEmpty 是否为空
- isNotEmpty 是否不为空
方法:
- remove(key) 删除指定key的数据
- addAll({…}) 合并映射 给映射内增加属性
- containsValue() 查看映射内的值 返回true/false
- forEach()
- map()
- where()
- any()
- every()
其它类型
void main() {
dynamic foo="bar";
print(foo);
foo=123;
print(foo);
}
- Set 无序集合,主要的功能就是去除数组重复内容
void main(){
List myList=['香蕉','苹果','西瓜','香蕉','苹果','香蕉','苹果'];
var s=new Set();
s.addAll(myList);
print(s);
print(s.toList());
}
- Future 用于异步支持。
- Stream 用于异步支持。
- Iterable 用在for-in 循环和同步生成器函数中。
- Never 表示表达式永远无法成功完成计算。最常用于总是抛出异常的函数。
- void 表示从未使用过某个值。通常用作返回类型。
- (value1, value2) 记录(需dart3)
- Null 空值
不常用的类型
runes 暴露了字符串的 Unicode 代码点(Unicode 为每个字母、数字和符号定义了一个唯一的数值)。例如,用于表情符号,(😆) 是
\u{1f606}
。经常被 “characters” API 取代。Symbol 表示标识符,例如
#
。
void main() {
assert(Symbol('bar')==#bar);
}
// true
类型转换
myint = int.parse(myString); // String -> int
myString = myint.toString(); // int -> String
mydouble = double.parse(myString); // String -> double
myString = 3.14159.toStringAsFixed(2); // double -> String (myString = '3.14')
mynum.member = mynum.values.byName(myString); // String -> enum
myString = mynum.member.name; // enum -> String
函数
bool isNoble(int atomicNumber) { // bool:返回类型
return _nobleGases[atomicNumber] != null;
}
箭头函数 =>
=> expr
是{ return expr; }
的简写
参数
- 命名参数(可传可不传)
String printUserInfo(String username, {int age = 0, String sex = '男'}) {//行参,用大括号
if (age != 0) {
return "姓名:$username---性别:$sex--年龄:$age";
}
return "姓名:$username---性别:$sex--年龄保密";
}
print(printUserInfo('张三')); //实参
print(printUserInfo('张三', age: 20, sex: '未知')); //实参,命名参数的实参必须传入参数名age: sex:
- 位置参数(可传可不传)
String printUserInfo(String username,[String sex='男',int age=0]){ //行参,用中括号
if(age!=0){
return "姓名:$username---性别:$sex--年龄:$age";
}
return "姓名:$username---性别:$sex--年龄保密";
}
print(printUserInfo('张三')); //实参
print(printUserInfo('小李','女',30)); //实参
运算符
算术运算符
+
(加)–
(减)-
(负)*
(乘)/
(除)~/
(取整)%
(取余)关系运算符
==
!=
>
<
>=
<=
逻辑运算符
!
(取反)&&
(与)||
(或)赋值运算符
基础赋值运算符
=
??=
(为 null 的变量赋值)复合赋值运算符
+=
-=
*=
/=
%=
~/=
其他运算符
()
使用一个方法[]
访问 List?[]
访问 List,左侧/?表示可以为null.
访问成员?.
访问成员,左侧/?表示可以为null..
级联,可以在同一个对象上访问实例成员和调用多个实例方法if-else的表达式:
- condition
?
expr1:
expr2
如果条件为真,则计算expr1(并返回其值);否则,计算并返回expr2的值。
- expr1
??
expr2
如果expr1不为 null,则返回其值;否则,计算并返回expr2的值。
- condition
类
方法
实例变量和方法
实例变量:
class Point {
double? x; // Declare instance variable x, initially null.
double? y; // Declare y, initially null.
double z = 0; // Declare z, initially 0.
}
实例方法可以访问实例变量和 this
import 'dart:math';
class Point {
final double x;
final double y;
Point(this.x, this.y);
double distanceTo(Point other) {
var dx = x - other.x;
var dy = y - other.y;
return sqrt(dx * dx + dy * dy);
}
}
静态变量和方法
使用关键字 static 可以声明类变量或类方法。静态成员可以通过类名称直接访问(不需要实例化),提高性能。静态方法不能访问非静态成员,非静态方法可以访问静态成员,不能使用this关键字。
get 和 set
特殊方法:实例对象的每一个属性都有一个隐式的 Getter 方法,非 final 属性还会有一个 Setter 方法。
- 通过get和set修饰的方法不带小括号,可以使访问方法像访问属性一样,简便我们的使用、访问
- set:传入属性,get:访问属性。
class Rectangle {
double left, top, width, height;
Rectangle(this.left, this.top, this.width, this.height);
double get right => left + width;
set right(double value) => left = value - width;
}
void main() {
var rect = Rectangle(3, 4, 20, 15);
// 通过getter访问 right
assert(rect.right == 23);
// 通过setter修改 right 属性
rect.right = 12;
assert(rect.left == -8);
}
抽象类、抽象方法
抽象类常用于定义接口,抽象类常常会包含抽象方法
abstract class Doer {
// Define instance variables and methods...
void doSomething(); // Define an abstract method.
}
class EffectiveDoer extends Doer {
void doSomething() {
// Provide an implementation, so the method is not abstract here...
}
}
call()
在别人项目经常看到此方法,看其解释感觉可以省略不写。
所有 Dart函数(具有函数类型而不是类/接口类型的对象)都有一个call方法。
该call方法与函数本身具有相同的函数类型,并且在调用它时它的行为完全相同。您甚至可以说调用函数就是隐式调用其call方法。如果您编写函数调用e1(e2, e3),那么编译器会检查是否e1有call方法,如果有,则将其转换为方法调用e1.call(e2, e3)。
构造函数
实例化类时会被自动触发
一般用于初始化操作
没有返回值
构造函数方法名和类名相同
class Point {
num x, y;
Point(num x, num y) {
// There's a better way to do this, stay tuned.
this.x = x;
this.y = y;
}
}
简化形式:
class Point {
num x, y;
// Syntactic sugar for setting x and y
// before the constructor body runs.
Point(this.x, this.y);
}
命名构造函数(类名.函数名)可以实现多个构造器。
class Point {
num x, y;
Point(this.x, this.y);
// 命名构造函数
Point.origin() {
x = 0;
y = 0;
}
}
请记住,命名构造函数不可继承,如果子类想要有 和父类一样的命名构造函数,那就写个同名的(通常也会在子类的命名构造函数里,调用父类的同名命名构造函数)
如果你的类,继承于父类,那么子类的构造函数,势必要调用父类的构造函数,这时候就要分两种情况:
- Dart语言帮你调用父类的无参数构造函数
- 代码中显式调用父类的构造函数
- 默认调用调用父类的无参数构造函数
如果你没有显式调用父类的构造函数,并且父类有一个无参数构造函数,那么Dart就会帮你在子类的构造函数方法体的最前面,调用父类的无参数构造函数。当然,后面我们会说道,构造函数分成好几部分来初始化成员变量,调用的顺序如下:
- 初始化列表
- 父类的无参数构造函数
- 子类的无参数构造函数
当然,如果父类没有无参数构造函数,或者Dart这种隐式调用无法满足你的要求,那就需要显式调用父类的构造函数了
- 显式调用父类构造函数
显式调用父类构造函数,应该在初始化列表中完成
class Person {
String firstName;
Person.fromJson(Map data) {
print('in Person');
}
}
class Employee extends Person {
// Person does not have a default constructor;
// you must call super.fromJson(data).
Employee.fromJson(Map data) : super.fromJson(data) {
print('in Employee');
}
}
main() {
var emp = new Employee.fromJson({});
// Prints:
// in Person
// in Employee
}
在构造函数后加上: x = , y =
可以在构造函数中设置属性的默认值
在构造函数体执行之前执行
可以调用超类的构造函数
// Initializer list sets instance variables before
// the constructor body runs.
Point.fromJson(Map<String, num> json)
: x = json['x'],
y = json['y'] {
print('In Point.fromJson(): ($x, $y)');
}
尤其是初始化那些final修饰的成员变量时,初始化列表很有用,因为在方法体中,不能给final修饰的成员变量赋值,因为在执行方法体的时候,final修饰的成员变量已经不能变了。这个地方很多人犯错。
import 'dart:math';
class Point {
final num x;
final num y;
final num distanceFromOrigin;
Point(x, y)
: x = x,
y = y,
distanceFromOrigin = sqrt(x * x + y * y);
}
main() {
var p = new Point(2, 3);
print(p.distanceFromOrigin);
}
定义构造函数的时候,除了一个普通构造函数,还可以有若干命名构造函数,这些构造函数之间,有时候会有一些相同的逻辑,如果分别书写在各个构造函数中,会有些多余,所以构造函数可以传递。
class Point {
num x, y;
// The main constructor for this class.
Point(this.x, this.y);
// Delegates to the main constructor.
Point.alongXAxis(num x) : this(x, 0);
}
传递构造函数,没有方法体,会在初始化列表中,调用另一个构造函数。
class ImmutablePoint {
static final ImmutablePoint origin =
const ImmutablePoint(0, 0);
final num x, y;
const ImmutablePoint(this.x, this.y);
}
如果你的类,创建的对象永远不会改变,你可以在编译期就创建这个常量实例,并且定义一个常量构造函数,并且确保所有的成员变量都是final的。
在构造函数前加上factory
只实例化一次,节省相同实例化带来的消耗
第一次调用命名构造函数进入工厂函数中实例化,后续调用就用缓存中现成的实例
工厂构造函数,没有权利访问this
class Logger {
final String name;
bool mute = false;
// _cache is library-private, thanks to
// the _ in front of its name.
static final Map<String, Logger> _cache =
<String, Logger>{};
factory Logger(String name) {
if (_cache.containsKey(name)) {
return _cache[name];
} else {
final logger = Logger._internal(name); # 调用构造函数
_cache[name] = logger;
return logger;
}
}
Logger._internal(this.name); # 定义命名构造函数
void log(String msg) {
if (!mute) print(msg);
}
}
main() {
var logger = Logger('UI');
logger.log('Button clicked');
}
上例的意思是,类中又一个静态缓存_cache
保存着一些Logger类实例,创建实例时,给工厂构造函数传递的name,如果在缓存中已经存在,就用缓存中现成的实例,如果没有,就新建一个实例,并且也放到缓存中。
如此,我们可以创建名字为UI / SYS / API 等的实例,然后在debug的时候,如果设置名字为UI的Logger实例的mute为true,就不会打印UI相关的log,而不影响其它两个名字的log。
回调函数
回调函数本质上是把函数作为参数传递给小部件的函数,当按钮按下时调用此函数。
官方实现:
/// Signature of callbacks that have no arguments and return no data.
typedef VoidCallback = void Function();
/// Signature for callbacks that report that an underlying value has changed.
/// See also:
/// * [ValueSetter], for callbacks that report that a value has been set.
typedef ValueChanged<T> = void Function(T value);
示例:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String topic = "Packages";
callback(varTopic) {
// setState:通知内部状态已更改从而重建UI
setState(() {
topic = varTopic;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text("Learning Flutter"),
),
body: Column(
children: [
Container(
width: double.maxFinite,
height: 70,
margin: const EdgeInsets.only(
top: 50, left: 40, right: 40, bottom: 20),
decoration: BoxDecoration(
color: Colors.lightBlue,
borderRadius: BorderRadius.circular(20)),
child: Center(
child: Text(
"We are learning Flutter $topic",
style: const TextStyle(fontSize: 20, color: Colors.white),
),
),
),
MyButtons(topic: "Cubit", callbackFunction: callback),
MyButtons(topic: "BLoc", callbackFunction: callback),
MyButtons(topic: "GetX", callbackFunction: callback)
],
),
),
);
}
}
class MyButtons extends StatelessWidget {
final String topic;
final Function callbackFunction;
const MyButtons(
{Key? key, required this.topic, required this.callbackFunction})
: super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
callbackFunction(topic);
},
child: Container(
width: double.maxFinite,
height: 70,
margin: const EdgeInsets.only(top: 20, left: 40, right: 40, bottom: 20),
decoration: BoxDecoration(
color: Colors.lightBlue, borderRadius: BorderRadius.circular(20)),
child: Center(
child: Text(
topic,
style: const TextStyle(fontSize: 20, color: Colors.white),
),
),
),
);
}
}
Function class
Function是所有函数类型的超类。
Function有一些没有声明的特殊功能:
- Function 静态类型的值仍然可以像函数一样被调用(这样的调用是动态调用,编译器无法在编译时检查类型的正确性,在运行时将执行检查以确保参数类型一致)。
Function f = (int x) => "$x";
print(f(1)); // Prints "1".
f("not", "one", "int"); // Throws! No static warning.
widgets之间的通信
https://medium.com/flutter-community/flutter-communication-between-widgets-f5590230df1e
扩展
扩展类
Extends是典型的OOP类继承。如果 a类 扩展了 b类,则 a类 可以使用或覆盖 b类 中实现的所有属性、变量、函数 。在 Dart 中,一个类只能扩展一个类。
创建另一个类或接口的实现。当 a类 实现 b类 时,必须实现 b类 中定义的所有功能(所有属性、变量、函数)。一个类可以实现多个接口。
mixin使用with关键字,类似于继承,可以扩展多个类。mixins的类只能继承自Object,不能再继承其他类,且不能有构造函数。
# extends只能扩展一个类,而mixin没有限制,可以一起使用
class Maestro extends Person with Musical, Aggressive, Demented {
Maestro(String maestroName) {
name = maestroName;
canConduct = true;
}
}
super
super
关键字用于调用父类的对象、方法、构造函数和子类中的属性。
// 访问父类变量
super.variable_name;
// 访问父类方法
super.method_name();
// 转发给超类的构造函数
const MyApp({Key? key}) : super(key: key);
扩展方法
// 扩展 num,就可以使用 num.attribute 和 num.method
extension FancyNum on num {
num plus(num other) => this + other;
num times(num other) => this * other;
}
print(5.plus(3)); // Equal to "5 + 3".
print(5.times(8)); // Equal to "5 * 8".
print(2.plus(1).times(3)); // Equal to "(2 + 1) * 3".
泛型
泛型常用于需要要求类型安全的情况,可以减少代码重复。
如果 T 是一个class,则表示传入该类型的参数。
@override 注解来表示你重写了一个成员
异步
Isolate 是运行所有 Dart 代码的地方,在许多其他语言里(例如 C++),你可以让多个线程共享相同的内存,但是在 Dart 中,每个线程都有自己的 Isolate 和它自己的内存。
如果你要运行的计算量太过庞大,在 main Isolate 中运行可能会导致丢帧,如果处理可能需要几百毫秒,则考虑创建单独的Isolate,例如:
- 解码 JSON,HttpRequest 的结果 => compute
- 加密可能非常耗时
- 处理图像(例如裁剪)
- 从 Web 加载图像
事件循环
Dart使用永不阻塞的单线程来处理所有事件。因此,它运行一个事件循环,它从事件队列中取得最先发生的事件,处理它,返回下一个事件进行处理,依此类推,直到事件队列清空为止。
定义
// Future<type>是一种类型
Future<void> myVoidFuture() {} // 不返回任何内容,但可以在最终完成时通知调用者。
Future<bool> myTypedFuture() {} // 如果需要返回一个值,那么你传递给它一个类型。
Future 有两种状态:
- 未完成(Uncompleted): 你刚刚得到一个 Future,还未打开
首先,事件发生,事件循环获取事件,并调用你写的处理程序,得到一个返回的 Future,此时Future是关着的,此时 Future 未能完成,事件循环继续执行其他事件。 - 已完成
- Completed with a value: 打开了,带有一个值
等到数据抵达时,Future 得到数据并打开它,如果Future 完成并带有一个值,此时会触发你的then
回调。then
是你可以用来在每个 Future 上注册回调的实例方法,你可以用它创建一个函数,传入一个匹配 Future 类型的参数。 - Completed with an error: 打开了,抛出一个异常
如果Future 在完成时没有带一个值,你可以使用catchError
注册另一个回调,catchError 的工作方式和 then 一样,唯一不同的是它捕获异常而不是值。你甚至可以给它一个test
方法,你可以通过这种方式使用多个 catchError 方法,每种方法都会检查错误返回值的类型。
- Completed with a value: 打开了,带有一个值
void main() {
Future<int>.delayed (
Duration (seconds: 3),
() { return 100; },
).then((value) {
print(value);
}).catchError(
(err) {
print('Caught $err');
},
test: (err) => err.runtimeType == String,
).whenComplete (() {
print('All finished!');
});
print('waiting...');
}
Future方法
Future.value
如果你已经知道 Future 返回的值,你可以使用 Future.value 为构造函数命名,构建缓存服务时可以用这个Future.error
它需要一个异常对象和一个可选的堆栈跟踪Future.delayed
在运行函数和 Future 完成之前,指定等待时长,可以创建测试用的模拟网络服务
每个 future 单一地传递错误,或者数据,Streams随着时间的推移,可以传送零个、多个值,或者是错误。
import 'dart:io';
void main() {
createData();
}
Future<ProcessedData> createData() async {
try {
final id = await _loadFromDisk();
final data = await _fetchNetworkData(id);
return ProcessedData(data);
} on HttpException catch (err) {
print('Network error: $err');
return ProcessedData.empty();
} finally {
print('All done!');
}
}
Future<int> _loadFromDisk() async {
print('loadFromDisk');
return 0;
}
Future<String> _fetchNetworkData(int id) async {
print('NetworkData');
return 'NetworkData';
}
class ProcessedData {
ProcessedData(this.data);
final String data;
static Future<ProcessedData> empty() async {
var empty = ProcessedData('empty');
print(empty);
return empty;
}
}
上面官方示例介绍了连续的 await,这是更清晰的使用示例:
void main() async {
print(getMeSomeFood());
print(await getMeSomethingBetter());
maybeSomethingSweet().then((String value) {
print(value);
});
print('done');
}
Future<String> getMeSomeFood() async {
return "an apple";
}
Future<String> getMeSomethingBetter() async {
return "a burger?";
}
Future<String> maybeSomethingSweet() async {
return "a chocolate cake!!";
}
// output:
// Instance of ‘_Future<String>’ 不等待,返回一个future
// a burger? 等待future完成
// done
// a chocolate cake!! 不等待,先继续执行print('done'),future完成后再调用then
Single value | Zero or more values | |
---|---|---|
Sync: | int | Iterable |
Async: | Future | Stream |
当您需要延迟生成一系列值时,请考虑使用生成器函数。Dart 内置支持两种生成器函数:
实现一个同步生成器函数,将函数体标记为sync*
,使用yield
语句传递值:
Iterable<int> naturalsTo(int n) sync* {
int k = 0;
while (k < n) yield k++;
}
实现异步生成器函数,将函数体标记为async*
,使用yield
语句传递值:
Stream<int> asynchronousNaturalsTo(int n) async* {
int k = 0;
while (k < n) yield k++;
}
如果您的生成器是递归的,您可以使用以下方法提高其性能yield*
:
Iterable<int> naturalsDownFrom(int n) sync* {
if (n > 0) {
yield n;
yield* naturalsDownFrom(n - 1);
}
}
flutter
Widget生命周期
StatelessWidget生命周期源码图
Flutter 中万物皆为Widget,widget
类继承自DiagnosticableTree
。
DiagnosticableTree
即“诊断树”,主要作用是提供调试信息。
Flutter 中的 UI 或一堆小部件组成,通常称为小部件树,根据 Widget 树生成一个 Element 树,Widget 和 Element 是一一对应的,根据 Element 树生成 Render 树(渲染树),真正的布局和渲染逻辑在 Render 树中。
Widget.createElement()
:创建一个Element实例,记为element
。
BuildContext
是widget对应的Element,用于跟踪树中的每个小部件并定位它们及其在树中的位置。每个小部件的BuildContext
都传递给它们的build
方法。build
方法返回小部件呈现的小部件树。
context
参数是BuildContext类的一个实例,表示当前 widget 在 widget 树中的上下文,每一个 widget 都会对应一个 context 对象。在很多时候我们都需要使用context,比如获取主题:Theme.of(context).colorScheme.primary
。
Key
: [Key]是[Widget]、[Element]和[SemanticsNode]的标识符。Key可以保持组件之前的状态,比如在用户滑动时或者修改集合时,决定的条件在canUpdate()
方法中。使用 GlobalKey
时,Flutter 不仅会在树中查找与特定级别匹配的键,还会在整个应用程序中查找,GlobalKey就像全局变量。
canUpdate(...)
: newWidget
与oldWidget
的runtimeType
和key
同时相等时就会用new widget
去更新Element
对象的配置。如果 Type 相同但 Key 不同,则Element将被deactivated(释放,但它可能仍然存在)。如果 Type 不同,则Element将被disposed(永久删除)。
element.mount()
:调用createRenderObject
创建RenderObject,并使用attachRenderObject
将RenderObject关联到Element上。
使用
StatelessWidget 要求我们重写build
方法,它将state
作为输入并提供相应的 UI 显示在用户屏幕上:UI = build(state),当我们初始化一个StatelessWidget对象时会调用 build 方法。
class CounterWidget extends StatelessWidget {
final bool isLoading;
final int counter;
const CounterWidget({
required this.isLoading,
required this.counter,
});
@override
Widget build(BuildContext context) {
return isLoading ? CircularProgressIndicator() : Text('$counter');
}
}
StatefulWidget 要求我们重写createState
功能:
class MyHomePage extends StatefulWidget {
final bool isLoading;
final int counter;
const MyHomePage({
required this.isLoading,
required this.counter,
});
@override
State<MyHomePage> createState() {
return MyHomePageState();
}
}
createState()
会为每一个StatefulElement
创建一个State对象。示例中创建了MyHomePageState
对象,当MyHomePageState
初始化时,它会调用build
函数。
class MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CounterWidget(
isLoading: widget.isLoading,
counter: widget.counter,
),
),
);
}
}
widget.isLoading
、widget.counter
属性用于表示当前配置信息,并且在父级更新小部件时会被自动更新。setState
是State的方法。当我们调用setState方法时,build方法将立即再次被调用。
当你的组件是静态的,不需要更新自身状态,使用StatelessWidget。当你的组件需要根据事件或交互来动态更新自身状态,使用StatefulWidget。
Getx
状态管理
介绍
分类 | 状态管理器 | 消耗RAM | 结论 |
无状态页面 | GetView | 最少 | 相当于StatelessWidget,性能最好,最常用 |
简单的状态管理(一个页面的状态管理) | GetBuilder | 较少 | 相当于StatefulWidget,性能较好 |
反应式状态管理(全局的状态管理) | Obx | 稍多 | 反应式的基础层,较Getx简洁 |
GetX | 较多 | 可以灵活使用的反应式状态管理 | |
混合态管理 | MixinBuilder | 最多 | 在GetBuilder中插入一个Obx,既可以响应式更新、也可以手动更新 |
Controller
Getx状态管理的代码结构都可以分为GetXController(控制器层)和view(界面层),GetXController 可以进一步分为state(状态层/变量层),logic(逻辑层)。
└── sinup
├── controller.dart ──┤─ ─ logic.dart
│ │─ ─ state.dart
├── view.dart
initState
或dispose()
,而在 Controller 中我们可以使用相对应的操作:onStart
(开始)[不可覆盖]:组件在内存分配的时间点就会被调用,完成后会调用onInit方法onInit
(初始化):组件在内存分配后会被马上调用,适用于初始化 Controller(例如一些成员属性的初始化),详见Workers小节onReady
(加载完成):在 onInit 一帧后被调用,适合做一些导航进入的事件(例如对话框提示、SnackBar)或异步网络请求onClose
(控制器被释放):在onDelete
方法前调用、用于销毁controller
使用的资源(例如关闭事件监听,关闭流对象、动画)或者销毁可能造成内存泄露的对象(例如TextEditingController
,AniamtionController
)。也适用于将数据进行离线持久化。onDelete
(删除)[不可覆盖]:在controller
销毁前调用,将控制器从内存中删除
反应状态管理
反应变量
使用反应状态管理需要声明反应变量,你有3种方法可以把一个变量变成是 “可观察的”。
1 - 第一种是使用 Rx{Type}
。
// 建议使用初始值,但不是强制性的
final name = RxString('');
final isLogged = RxBool(false);
final count = RxInt(0);
final balance = RxDouble(0.0);
final items = RxList<String>([]);
final myMap = RxMap<String, int>({});
2 - 第二种是使用 Rx
,规定泛型 Rx<Type>
。
final name = Rx<String>('');
final isLogged = Rx<Bool>(false);
final count = Rx<Int>(0);
final balance = Rx<Double>(0.0);
final number = Rx<Num>(0)
final items = Rx<List<String>>([]);
final myMap = Rx<Map<String, int>>({});
// 自定义类 - 可以是任何类
final user = Rx<User>();
3 - 第三种更实用、更简单和首选的方法,只需添加 .obs
作为value
的属性。
final name = ''.obs;
final isLogged = false.obs;
final count = 0.obs;
final balance = 0.0.obs;
final number = 0.obs;
final items = <String>[].obs;
final myMap = <String, int>{}.obs;
// 自定义类 - 可以是任何class, literally
final user = User().obs;
使用value
// controller
final count1 = 0.obs;
final count2 = 0.obs;
int get sum => count1.value + count2.value; // 由于.obs把变量换成`Rx{Type}`类型,所以需要用.value取值
// 视图
GetX<Controller>(
builder: (controller) {
print("count 1 rebuild");
return Text('${controller.count1.value}');
},
),
GetX<Controller>(
builder: (controller) {
print("count 2 rebuild");
return Text('${controller.count2.value}');
},
),
GetX<Controller>(
builder: (controller) {
print("count 3 rebuild");
return Text('${controller.sum}');
},
),
Obx
class StateObxView extends StatelessWidget {
StateObxView({Key? key}) : super(key: key);
final count = 0.obs;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Obx(...)"),
),
body: Center(
child: Column(
children: [
Obx(() => Text("count1 -> " + count.toString())),
Obx(() => Text("count2 -> " + count.toString())),
//
Divider(),
ElevatedButton(
onPressed: () {
count.value++;
},
child: Text('add'),
),
],
),
),
);
}
}
GetX
Controller
class CountController extends GetxController {
final _count = 0.obs;
set count(value) => this._count.value = value;
get count => this._count.value;
final _count2 = 0.obs;
set count2(value) => this._count2.value = value;
get count2 => this._count2.value;
add() => _count.value++;
add2() => _count2.value++;
}
View
class StateGetxView extends StatelessWidget {
StateGetxView({Key? key}) : super(key: key);
final controller = CountController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Getx"),
),
body: Center(
child: Column(
children: [
GetX<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
print("GetX - 1");
return Text('value 1 -> ${_.count}');
},
),
GetX<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
print("GetX - 2");
return Text('value 2 -> ${_.count}');
},
),
Divider(),
//
GetX<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
print("GetX - 3");
return Column(
children: [
Text('value 3 -> ${_.count}'),
ElevatedButton(
onPressed: () {
_.add();
},
child: Text('count1'),
)
],
);
},
),
Divider(),
// count2
GetX<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
print("GetX - 4");
return Text('value 4 -> ${_.count2}');
},
),
Divider(),
// 按钮
ElevatedButton(
onPressed: () {
controller.add();
},
child: Text('count1'),
),
ElevatedButton(
onPressed: () {
controller.add2();
},
child: Text('count2'),
),
],
),
),
);
}
}
简单状态管理
GetBuilder
Controller同Getx
View
class StateGetBuilderView extends StatelessWidget {
StateGetBuilderView({Key? key}) : super(key: key);
final controller = CountController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("GetBuilder"),
),
body: Center(
child: Column(
children: [
GetBuilder<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
print("GetBuilder - 1");
return Text('value -> ${_.count}');
},
),
GetBuilder<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
print("GetBuilder - 2");
return Text('value -> ${_.count}');
},
),
Divider(),
//
GetBuilder<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
print("GetBuilder - 3");
return Column(
children: [
Text('value -> ${_.count}'),
ElevatedButton(
onPressed: () {
_.add();
},
child: Text('GetBuilder -> add'),
)
],
);
},
),
Divider(),
// count2
GetBuilder<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
print("GetBuilder - 4");
return Text('value count2 -> ${_.count2}');
},
),
Divider(),
// id2 标记一个 `builder` ,触发方式`controller.update(["id2"]);` ,可传多个 `Array` 类型。
GetBuilder<CountController>(
id: "id2",
init: controller,
initState: (_) {},
builder: (_) {
print("GetBuilder - 4");
return Text('id2 -> value count2 -> ${_.count2}');
},
),
Divider(),
// 按钮
ElevatedButton(
onPressed: () {
controller.add();
},
child: Text('add'),
),
ElevatedButton(
onPressed: () {
controller.add2();
},
child: Text('add2'),
),
ElevatedButton(
onPressed: () {
controller.update();
},
child: Text('controller.update()'),
),
ElevatedButton(
onPressed: () {
controller.update(["id2"]);
},
child: Text('controller.update(id2)'),
),
],
),
),
);
}
}
Workers
Workers可以精确控制事件发生时触发回调,常用于Controller的onInit中:
class CountController extends GetxController {
final _count = 0.obs;
set count(value) => this._count.value = value;
get count => this._count.value;
add() => _count.value++;
@override
void onInit() {
super.onInit();
// 每次_count变化时调用
ever(_count, (value) {
print("ever -> " + value.toString());
});
// 第一次被改变时才会被调用。
once(_count, (value) {
print("once -> " + value.toString());
});
// 防DDos - 每当用户停止输入1秒时调用
debounce(
_count,
(value) {
print("debounce -> " + value.toString());
},
time: Duration(seconds: 1),
);
// 忽略1秒内的所有变化。
interval(
_count,
(value) {
print("interval -> " + value.toString());
},
time: Duration(seconds: 1),
);
}
}
路由管理
普通路由
// 导航到新的页面。
Get.to(NextScreen());
// 要导航到下一条路由,并在返回后立即接收或更新数据。
var data = await Get.to(Payment());
// 关闭SnackBars、Dialogs、BottomSheets或任何你通常会用Navigator.pop(context)关闭的东西。
Get.back();
// 在另一个页面上,发送前一个路由的数据。并使用它。
Get.back(result: 'success');
if(data == 'success') madeAnything();
// 进入下一个页面,但没有返回上一个页面的选项(用于SplashScreens,登录页面等)。
Get.off(NextScreen());
// 进入下一个界面并取消之前的所有路由(在购物车、投票和测试中很有用)。
Get.offAll(NextScreen());
别名路由
普通路由管理起来比较麻烦,通常我们使用别名路由。
// 导航到下一个页面
Get.toNamed("/NextScreen");
// 传递参数
Get.toNamed("/NextScreen", arguments: "Hello");
// 浏览并删除前一个页面。
Get.offNamed("/NextScreen");
// 浏览并删除所有以前的页面。
Get.offAllNamed("/NextScreen");
// 动态URL
Get.offAllNamed("/NextScreen?device=phone&id=354&name=Enzo");
示例 1
注册时跳到到Pin页面,路由传参:
// RegisterController
Get.offNamed(
RouteNames.systemRegisterPin,
arguments: UserRegisterReq(
username: userNameController.text,
email: emailController.text,
password: password,
),
);
从路由接收参数:
// RegisterPinController
UserRegisterReq? req = Get.arguments;
依赖注入
Get.put(): 不使用控制器实例也会被创建
Get.put(CountController());
Get.lazyPut(): 懒加载方式创建实例,只有在使用时才创建
Get.lazyPut<CountController>(() => CountController());
Get.putAsync(): Get.put()
的异步版版本
Get.putAsync<CountController>(() async => await CountController());
Get.create(): 每次使用都会创建一个新的实例
Get.find(): 你可以实例化100万个控制器,Get总会找到你所需的控制器
Get.find<CountController>();
其他
GetView
一个已注册Controller的const Stateless
Widget。
GetxService
这个类就像一个GetxController,需要在应用程序的生命周期绝对持久化类实例,使用GetxService。
其他高级 API
https://github.com/jonataslaw/getx#other-advanced-apis
dio
原理
# dio_mixin.dart 文件中 DioMixin 实现了 Dio
abstract class DioMixin implements Dio {
@override
Future<Response<T>> post<T>(
String path, { # path: 请求的url链接
data, # data: 请求数据,例如上传用到的FromData
Map<String, dynamic>? queryParameters, # data: 请求数据,例如上传用到的FromData
Options? options, # queryParameters: 查询参数
CancelToken? cancelToken, # cancelToken: 用来取消发送请求的token
ProgressCallback? onSendProgress, # onSendProgress: 网络请求发送的进度
ProgressCallback? onReceiveProgress, # onSendProgress: 网络请求发送的进度
}) {
return request<T>( # 返回request方法
path,
data: data,
options: checkOptions('POST', options),
queryParameters: queryParameters,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
}
get() post() 等调用时,返回request方法,request 方法对请求参数处理,并返回 fetch 方法,fetch 进行响应数据设定、构建请求流、添加拦截器、请求分发。
WebView
WebView是应用内显示网页的功能,查看项目的官网、文档、条款等经常用到。推荐使用flutter_inappwebview,比官方webview_flutter有更丰富的功能和详尽的文档。主要功能:
InAppWebView:是一个内嵌原生 WebView 小部件,集成到 Flutter 小部件树中。
- ContextMenu:WebView 的快捷菜单。例如长按网页文本后的复制。
- HeadlessInAppWebView:无头模式下的 WebView。在没有界面或UI的情况下运行WebView。它可以在没有用户界面的情况下执行网页加载和渲染操作,而不需要在用户界面中显示网页,用于后台处理网页数据、自动化测试、网络爬虫等。
InAppBrowser
- InAppBrowser:显示在 Flutter 应用程序顶部的原生 WebView,它没有集成到 Flutter 小部件树中。
- ChromeSafariBrowser:Android 上的 Chrome 自定义选项卡和 iOS 上的SFSafariViewController。
InAppLocalhostServer:这个类允许你在 http://localhost:[port] 上创建一个简单的服务器。默认端口值为 8080。能够在本地服务器上缓存 js,html等资产文件,优化加载时间。
CookieManager:此类实现了一个单例对象(共享实例),该对象管理 WebView 实例使用的 cookie。
HttpAuthCredentialDatabase:此类实现管理共享 HTTP 身份验证凭据缓存的单例对象(共享实例)。
WebStorageManager:这个类实现了一个单例对象(共享实例),它管理 WebView 实例使用的 Web 存储。
Service Worker:Service Worker 是 PWA 的基本组成部分。它们支持快速加载(无论网络如何)、离线访问、推送通知和其他功能。
InAppWebView官方示例注释
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
// 如果是Android,则对加载到 WebView 中的 Web内容(HTML/CSS/JavaScript)启用调试
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
await InAppWebViewController.setWebContentsDebuggingEnabled(true);
}
runApp(const MaterialApp(home: MyApp()));
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final GlobalKey webViewKey = GlobalKey();
InAppWebViewController? webViewController;
InAppWebViewSettings settings = InAppWebViewSettings(
// 能够监听 WebView.shouldOverrideUrlLoading(URL即将加载时)的事件。
useShouldOverrideUrlLoading: true,
// 设置为 true 以防止 HTML5 音频或视频自动播放。
mediaPlaybackRequiresUserGesture: false,
// 允许 HTML5 媒体播放在屏幕布局中内嵌显示,即让媒体播放器嵌入到网页的其他内容中一起显示
allowsInlineMediaPlayback: true,
// 根据请求的来源定义了哪些功能可供使用(例如,访问麦克风、摄像头、电池、网络共享等)
iframeAllow: "camera; microphone",
// 如果 iframe 可以通过调用 requestFullscreen() 方法激活全屏模式,则设置为 true。
iframeAllowFullscreen: true
);
PullToRefreshController? pullToRefreshController;
String url = "";
double progress = 0;
final urlController = TextEditingController();
@override
void initState() {
super.initState();
// 在 WebView 中开启下拉刷新
pullToRefreshController = kIsWeb ? null : PullToRefreshController(
settings: PullToRefreshSettings(
color: Colors.blue,
),
onRefresh: () async {
if (defaultTargetPlatform == TargetPlatform.android) {
webViewController?.reload();
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
webViewController?.loadUrl(
urlRequest: URLRequest(url: await webViewController?.getUrl()));
}
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Official InAppWebView website")),
body: SafeArea(
child: Column(children: <Widget>[
TextField(
decoration: const InputDecoration(prefixIcon: Icon(Icons.search)),
controller: urlController,
keyboardType: TextInputType.url,
onSubmitted: (value) {
var url = WebUri(value);
if (url.scheme.isEmpty) {
url = WebUri("https://www.google.com/search?q=$value");
}
webViewController?.loadUrl(urlRequest: URLRequest(url: url));
},
),
Expanded(
child: Stack(
children: [
InAppWebView(
key: webViewKey,
// 初始化URL
initialUrlRequest:
URLRequest(url: WebUri("https://inappwebview.dev/")),
// 初始化设置
initialSettings: settings,
// 下拉刷新
pullToRefreshController: pullToRefreshController,
// 创建 WebView 时触发的事件
onWebViewCreated: (controller) {
webViewController = controller;
},
// 当 WebView 开始加载 url 时触发的事件。
onLoadStart: (controller, url) {
setState(() {
this.url = url.toString();
urlController.text = this.url;
});
},
// 请求无访问权限资源 时触发的事件。
onPermissionRequest: (controller, request) async {
return PermissionResponse(
resources: request.resources,
action: PermissionResponseAction.GRANT);
},
// 让APP有机会在 URL即将加载 时进行控制。
shouldOverrideUrlLoading:
(controller, navigationAction) async {
var uri = navigationAction.request.url!;
if (![
"http",
"https",
"file",
"chrome",
"data",
"javascript",
"about"
].contains(uri.scheme)) {
if (await canLaunchUrl(uri)) {
// Launch the App
await launchUrl(
uri,
);
// and cancel the request
return NavigationActionPolicy.CANCEL;
}
}
return NavigationActionPolicy.ALLOW;
},
// 完成加载 url 时触发的事件。
onLoadStop: (controller, url) async {
pullToRefreshController?.endRefreshing();
setState(() {
this.url = url.toString();
urlController.text = this.url;
});
},
// 加载请求时遇到错误 时触发的事件。
onReceivedError: (controller, request, error) {
pullToRefreshController?.endRefreshing();
},
// 更改正在加载的页面 时触发的事件。
onProgressChanged: (controller, progress) {
if (progress == 100) {
pullToRefreshController?.endRefreshing();
}
setState(() {
this.progress = progress / 100;
urlController.text = url;
});
},
// APP更新其访问的链接 时触发的事件。
onUpdateVisitedHistory: (controller, url, androidIsReload) {
setState(() {
this.url = url.toString();
urlController.text = this.url;
});
},
// WebView收到控制台报错 时触发的事件。
onConsoleMessage: (controller, consoleMessage) {
if (kDebugMode) {
print(consoleMessage);
}
},
),
// 进度指示器
progress < 1.0
? LinearProgressIndicator(value: progress)
: Container(),
],
),
),
ButtonBar(
alignment: MainAxisAlignment.center,
children: <Widget>[
// 后退按钮
ElevatedButton(
child: const Icon(Icons.arrow_back),
onPressed: () {
webViewController?.goBack();
},
),
// 前进按钮
ElevatedButton(
child: const Icon(Icons.arrow_forward),
onPressed: () {
webViewController?.goForward();
},
),
// 刷新按钮
ElevatedButton(
child: const Icon(Icons.refresh),
onPressed: () {
webViewController?.reload();
},
),
],
),
])
)
);
}
}
FlexColorPicker
FlexColorPicker 是 Flutter 的可定制颜色选择器。可以使用 Material 2 和 Material 3。
// 属性
ColorPicker(
color: selectedColor,
// 启用tab栏选择器:both, primary, accent, bw, custom, wheel
pickersEnabled: const <ColorPickerType, bool>{
ColorPickerType.both: true,
ColorPickerType.primary: false,
ColorPickerType.accent: false,
ColorPickerType.bw: false,
ColorPickerType.custom: false,
ColorPickerType.wheel: true,
},
// 自定义颜色
// customColorSwatchesAndNames: customSwatches,
// 定制tab标签
pickerTypeLabels: <ColorPickerType, String>{
ColorPickerType.both: 'preset'.tr,
ColorPickerType.wheel: 'custom'.tr,
},
// 在选择主要颜色后,基于所选颜色为您提供一组它的深浅颜色,禁用时才需要此属性
// enableShadesSelection:false,
// 生成所选颜色的 15 种色调的 Material 3 色调调色板
enableTonalPalette: true,
// 颜色不透明度滑块
enableOpacity: true,
// 显示所选颜色名称
showMaterialName: true,
materialNameTextStyle: Theme.of(context).textTheme.bodySmall,
showColorName: true,
colorNameTextStyle: Theme.of(context).textTheme.bodySmall,
// 显示所选颜色的 RGB 颜色值
showColorCode: true,
// colorCodeHasColor: true,
// showColorValue: true,
colorCodeTextStyle: Theme.of(context).textTheme.bodyMedium,
colorCodePrefixStyle: Theme.of(context).textTheme.bodySmall,
// 显示最近选择的颜色
// showRecentColors: true,
// maxRecentColors: 8,
// recentColors: ,
// onRecentColorsChanged: ,
// 标题
title: Text(
'ColorPicker',
style: Theme.of(context).textTheme.headline6,
),
heading: Text(
'Select color',
style: Theme.of(context).textTheme.headline5,
),
subheading: Text(
'Select color shade',
style: Theme.of(context).textTheme.headline1,
),
wheelSubheading: Text(
'Selected color and its shades',
style: Theme.of(context).textTheme.headline1,
),
opacitySubheading: Text(
'Opacity',
style: Theme.of(context).textTheme.headline1,
),
recentColorsSubheading: Text(
'Selected color and its color swatch',
style: Theme.of(context).textTheme.headline1,
),
// 各个颜色小部件的大小、形状和间距以及色轮和不透明度滑块大小的属性。
width: 40,
height: 40,
borderRadius: 4,
spacing: 5,
runSpacing: 5,
hasBorder: false,
// borderColor: Theme.of(context).dividerColor,
elevation: 0,
// 轮盘直径、宽度和边框
wheelDiameter: 155,
wheelWidth: 16,
wheelHasBorder: false,
// borderColor: Theme.of(context).dividerColor,
// 颜色元素的间距、对齐方式和填充
crossAxisAlignment: CrossAxisAlignment.center,
padding: const EdgeInsets.all(16),
columnSpacing: 0,
// 复制按钮,确定按钮,取消按钮,关闭按钮
enableTooltips: ture,
// Dialog“确定”和“取消”操作按钮以及样式
actionButtons: const ColorPickerActionButtons(...),
// 颜色的复制粘贴行为
copyPasteBehavior: const ColorPickerCopyPasteBehavior(
// 颜色代码后缀复制按钮
editFieldCopyButton: ture,
// 键盘快捷键
ctrlC: ture,
ctrlV: ture,
// 工具栏按钮
copyButton: ture,
copyIcon: ture,
copyTooltip: MaterialLocalizations.of(context).copyButtonLabel,
pasteButton: ture,
pasteIcon: ture,
pasteTooltip: MaterialLocalizations.of(context).pasteButtonLabel,
// 从选取器复制颜色并将颜色粘贴
longPressMenu: ture,
secondaryMenu: ture,
secondaryOnDesktopLongOnDevice: ture,
// 颜色代码格式和粘贴解析
copyFormat: ColorPickerCopyFormat.dartCode,
parseShortHexCode: ture,
editUsesParsedPaste: ture,
snackBarParseError: ture,
snackBarMessage: ture,
snackBarDuration: const Duration(milliseconds: 1800),
feedbackParseError: false,
),
// 回调
// 当选择具有所选新颜色值的新颜色时调用
onColorChanged: (Color color) {
selectedColor = color;
},
// 开始颜色选择时调用
onColorChangeStart: ,
// 结束颜色选择时调用
onColorChangeEnd: ,
// 返回最近选择的颜色的当前列表
onRecentColorsChanged: ,
),
// dialog方法:
// 在对话框打开时跟踪 ColorPicker 的不同onChange回调
ColorPicker(...).showPickerDialog;
// dialog函数:
// 只需传入对话框的构建上下文以及所需的起始颜色值,并等待它在对话框关闭时返回选定的颜色
showColorPickerDialog(...)
Widgetbook(仅作了解)
使用Widgetbook可以管理组件、使用不同设备尺寸测试组件、实时修改组件参数、协作共享。
Windows配置
确保安装Visual Studio,及其应用:
从https://www.nuget.org/downloads下载最新的nuget.exe,并放入任意文件夹(我的在C:\Program Files\Microsoft Visual Studio),配置系统环境变量。
使用
- 在pubspec.yaml安装 widgetbook 组件
注意是放在 dev_dependencies 下面
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
widgetbook: ^3.0.0-beta.14
- 新建lib/app.widgetbook.dart
import 'package:flutter/material.dart';
// ignore: depend_on_referenced_packages
import 'package:widgetbook/widgetbook.dart';
import '你的组件';
void main() {
runApp(const HotReload());
}
class HotReload extends StatelessWidget {
const HotReload({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Widgetbook.material(
// Widgetbook 属性选择
addons: [
// 主题
// buildMaterialThemeAddon(),
// 字体大小
// buildTextScaleAddon(),
],
// 可以容纳包、文件夹、类别、组件和用例
directories: [
//
buildWidgetbookCategory(),
//
//buildWidgetbookCategory2(),
],
);
}
TextScaleAddon buildTextScaleAddon() {
return TextScaleAddon(
setting: TextScaleSetting.firstAsSelected(
textScales: [1.0, 1.25, 1.5, 1.75, 2]));
}
MaterialThemeAddon buildMaterialThemeAddon() {
return MaterialThemeAddon(
setting: MaterialThemeSetting.firstAsSelected(themes: [
WidgetbookTheme(name: "dark", data: ThemeData.dark()),
WidgetbookTheme(name: "light", data: ThemeData.light()),
]));
}
WidgetbookCategory buildWidgetbookCategory() {
return WidgetbookCategory(
// 分类名
name: '公共组件',
children: [
WidgetbookComponent(
// 组件名
name: 'Spinkit加载',
useCases: [
WidgetbookUseCase.center(
// 组件用例名
name: "SpinKitFadingCircle",
child: mySpinkit(),
),
],
),
],
);
}
}
Plugin
Flutter Plugin是可以插入到主程序中,扩展功能的独立组件,我们实现原生平台的功能就需要用到。
创建
使用 Android Studio 创建 Flutter 项目,项目类型选 plugin 。目录如下
project
├── android // 原生
├── example // 运行调试
├── ios // 原生
├── lib
Flutter
Flutter有三种类型的Channel:
- BasicMessageChannel:用于传递字符串和半结构化的信息。持续通信,收到消息后可以回复此次消息,如:Native将遍历到的文件信息陆续传递到Dart。
- MethodChannel:用于传递方法调用(method invocation)。一次性通信:如Flutteri调用Native拍照;
- EventChannel:用于数据流(event streams)的通信。持续通信,收到消息后无法回复此次消息,通过长用于Native向Dat的通信,如:手机电量变化,网络连接变化,陀螺仪,传感器等;
Channel支持的数据类型:
Dart | Android | ios |
---|---|---|
null | null | nil (NSNull when nested) |
bool | java.lang.Boolean | NSNumber numberWithBool: |
int | java.lang.Integer | NSNumber numberWithInt: |
int, if 32 bits notenough | java.lang.Long | NSNumber numberWithLong: |
double | java.lang.Double | NSNumber numberWithDouble: |
String | java.lang.String | NSString |
Uint8List | byte[] | FlutterStandardTypedData typedDataWithBytes: |
Int32List | int][] | FlutterStandardTypedData typedDataWithInt32: |
Int64List | long[] | FlutterStandardTypedData typedDataWithInt64: |
Float64List | double[] | FlutterStandardTypedData typedDataWithFloat64: |
List | java.util.ArrayList | NSArray |
Map | java.util.HashMap | NSDictionary |
Android
Java
- 访问修饰符
访问范围 | private | default | protected | public |
---|---|---|---|---|
同一类中 | √ | √ | √ | √ |
同一包中(子类和非子类) | × | √ | √ | √ |
不同包中的子类 | × | × | √ | √ |
不同包中的非子类 | × | × | × | √ |
进程和线程
进程
- Android系统会为每个应用程序创建一个进程
- 如果该应用程序的进程已经存在(已有组件已经在运行),那么刚启动的组件会在已有的进程和线程中启动运行
- 组件可以运行在指定的其他线程,在AndroidManifest文件中的每种组件标签都支持设置 android:process 属性
- 系统会依据进程的“importance hierarchy”等级清除进程,这是为了回收系统资源和新建进程
- Binder/Socket用于进程间通信
线程
- 系统会为应用程序创建一个名为“main”的主线程,不会为每个组件的实例创建单独的线程
- 通常实现Runnable接口来定义线程的执行逻辑,然后将其传递给Thread类来启动线程
- Handler用于同进程的线程间通信,子线程运行并生成Message,Looper获取message并传递给Handler,Handler逐个获取子线程中的Message
概念 | 定义 | 作用 |
---|---|---|
主线程 (UI线程、Main Thread) | 当应用程序启动时,会自动开启1条主线程 | 处理与UI相关的事件(如更新、操作等) |
子线程 (工作线程) | 人为手动开启的线程 | 执行耗时操作(如网络请求、数据加载等) |
消息 (Message) | 线程间通讯的数据单元 (即Handler接受&处理的消息对象) | 存储需操作的通信信息 |
消息队列 (Message Queue) | 一种数据结构(存储特点:先进先出) | 存储Handler发送过来的消息(Message) |
处理者 (Handler) | 主线程与子线程的通信媒介线程消息的主要处理者 | * 添加消息(Message)到消息队列(Message Queue) *处理循环器(Looper)分派过来的消息(Message) |
循环器 (Looper) | 消息队列(Message Queue)与处理者(Handler)的通信媒介 每个线程中只能拥有1个Looper,多个线程可往1个Looper所持有的MessageQueue 中发送消息,提供了线程间通信的可能 | * 消息获取:循环取出消息队列(Message Queue)的消息(Message) * 消息分发:将取出的消息(Message)发送给对应的处理者(Handler) |
View.post() | View类中的方法,适用于任何View对象 | * 将Runnable对象添加到View的事件队列中 * 获取View的宽高等属性值 |
ThreadLocal | 提供线程本地变量 | ThreadLocal为每一个线程开辟了一个独立的存储器,只有对应的线程才能够访问其数据 |
runOnUiThread() | Activity类中的方法,仅适用于Activity | 将Runnable对象添加到Activity的事件队列中,可以确保其在当前主线程中执行 |
生命周期
View的生命周期方法
类别 | 方法 | 描述 |
---|---|---|
创建 | 构造函数 | 构造函数有两种形式,一种是在通过代码创建视图时调用的,另一种是在通过布局文件填充视图时调用的。第二种形式解析并应用布局文件中定义的属性。 |
| 在视图及其所有子视图都从 XML 中映射后调用。 | |
布局 |
| 调用来确定此视图及其所有子视图的尺寸要求。 |
| 当此视图必须为其所有子视图分配大小和位置时调用。 | |
| 当此视图的大小改变时调用。 | |
绘画 |
| 当视图必须呈现其内容时调用。 |
事件处理 |
| 当发生按键事件时调用。 |
| 当发生按键释放事件时调用。 | |
| 当轨迹球运动事件发生时调用。 | |
| 当发生触摸屏运动事件时调用。 | |
焦点 |
| 当视图获得或失去焦点时调用。 |
| 当包含视图的窗口获得或失去焦点时调用。 | |
附加 |
| 当视图附加到窗口时调用。 |
| 当视图与其窗口分离时调用。 | |
| 当包含视图的窗口的可见性发生改变时调用。 |
Activity生命周期
Activity的启动
- Activity 调用onCreate方法,将资源 ID
R.layout.main_activity
更改为setContentView()
- Activity 调用onWindowAttributesChanged 方法,而且这个方法连续调用多次
View
调用构造方法View
调用onFinishInflate方法,说明这个时候View已经填充完毕,但是还没开始触发绘制过程- Activity 调用onstart方法, 进入“已启动”状态
- Activity 再次调用 onWindowAttributesChanged 方法
- Activity 调用onResume,“已恢复”状态,进行后面初始化步骤,并进入与用户互动的状态
- Activity 调用onAttachedToWindow,Activity跟Window进行绑定
View
调用onAttachedToWindow,View跟Window进行绑定View
调用 onWindowVisibilityChanged(int visibility),参数变为View.VISIABLE
View
调用onMeasure,开始测量View
调用onSizeChanged,表示测量完成,尺寸发生了变化View
调用onLayout,开始摆放位置View
调用 onDraw,开始绘制- Activity 调用onWindowFocusChanged(boolean hasFocus),此时为true,代表窗体已经获取了焦点
View
调用 onWindowFocusChanged(boolean hasWindowFocus),此时为true,代表当前的控件获取了Window焦点,当调用这个方法后说明当前Activity中的View才是真正的可见了
Activity的退出
- Activity 调用 onPause,中断时的暂停状态
View
调用 onWindowVisibilityChanged(int visibility),参数变为View.GONE
,View中对应的Window隐藏- Activity 调用onWindowFocusChanged(boolean hasFocus),此时为false,说明Actvity所在的Window已经失去焦点
- Activity 调用 onStop,此时Activity已经切换到后台
- Activity 调用 onDestory,此时Activity开始准备销毁,实际上并不代表Activity已经销毁
View
调用 onDetachedFromWindow,此时View 与Window解除绑定- Activity 调用 onDetathedFromWindow ,此时Activity 与Window 解除绑定
- View即将被销毁,可以在
onDetachedFromWindow
方法中做一些资源释放,防止内存泄漏
android四大组件
activity
每个 Activity 提供一个用户界面窗口,一个app 由多个Activity 组成,其中一个主 Activity为程序入口。
- 应用窗口(
TYPE_APPLICATION
):Activity 的默认窗口,层级最低。 - 子窗口(
TYPE_APPLICATION_PANEL
):依附于应用窗口(如 Dialog)。 - 系统窗口(
TYPE_TOAST
):无需 Activity 承载,直接由系统管理(如 Toast、悬浮窗)。
- 应用窗口(
Activity通过
Intent
和Bundle
实现数据传递。每一个Activity都必须要在AndroidManifest.xml配置。
service
Service通常位于后台运行,没有UI。
service分为两种
- started(启动):由其他组件调用startService()方法启动,可以在后台无限期运行,调用stopSelf()或由其他组件调用stopService()方法才会停止。
- bound(绑定):调用bindService()者与服务绑定在了一起,调用者一旦退出,服务也就终止。
必须在AndroidManifest.xml配置
<service android:name=".ExampleService"/>
content provider
Android 系统为常见数据类型(如视频、音频、图像、电话簿等)提供了 ContentProvider 接口,用于应用程序之间共享数据。
通过 URI 标识数据资源,并通过 ContentResolver 提供对数据的 CRUD 操作。
broadcast receiver
- 事件触发:当特定事件发生时(如来电、短信、电池电量变化等),Android系统会生成特定的Intent对象并自动进行广播。
- 接收处理:针对特定事件注册的BroadcastReceiver会接收到这些广播,并获取Intent对象中的数据进行处理。
- 动态注册
- 静态注册:AndroidManifest文件
builder模式
当一个对象有很多属性,比如用户对象有很多属性:用户名、ID、性别、地址、工作类型、联系方式等等,写多个构造方法和set、get将难以阅读维护,我们可以采用java链式调用这种更优雅的方式。
public class User {
private final String name;
private int age;
private String address;
private User(Builder builder) {
// 初始化变量
this.name = builder.name;
this.age = builder.age;
this.address = builder.address;
}
static class Builder {
// 与外部User类属性一致
private final String name;
private int age;
private String address;
// 必填属性
public Builder(String name) {
this.name = name;
}
// 可选属性
public Builder Age(int age) {
this.age = age;
return this;
}
// 可选属性
public Builder Address(String address) {
this.address = address;
return this;
}
// 提供外部类实例对象
public User build() {
// 合理性做判断
if (age <= 0) {
throw new RuntimeException("年龄不合法");
}
return new User(this);
}
}
}
//使用
User user = new User.Builder("UserName")
.Age(18)
.Address("China")
.build();
另外,可以使用IDE中的插件InnerBuilder生成代码。
// 在类中右键Generate选择Builder
public class User {
private final String name;
private int age;
private String address;
}
Channel
创建Channel
三种Channel 的构造十分相似:
// 传递字符串、JSON
BasicMessageChannel(BinaryMessenger messenger,String name,MessageCodec<T> codec)
// 方法调用
// MethodChannel只是对 BasicMessageChannel封装,方便直接获取函数名和参数
MethodChannel(BinaryMessenger messenger,String name,MethodCodec codec)
// 事件流监听,包括用户事件监听,电量变化,网络连接变化,陀螺仪,传感器等
EventChannel(BinaryMessenger messenger,String name,MethodCodec codec)
BinaryMessenger messenger
消息的发送与接收的工具;String name
Channel的名字;MessageCodec<T> codec
消息的编解码器,它有几种不同类型的实现:BinaryCodec
最为简单的一种Codec,因为其返回值类型和入参的类型相同,均为二进制格式(Android中为ByteBuffer,.iS中为NSData)。实际上,BinaryCodec在编解码过程中什么都没做,只是原封不动将二进制数据消息返回而已。或许你会因此觉得BinaryCodec没有意义,但是在某些情况下它非常有用,比如使用BinaryCodec可以使传递内存数据块时在编解码阶段免于内存拷贝;StringCodec
用于字符串与二进制数据之间的编解码,其编码格式为UTF-8;JSONMessageCodec
用于基础数据与二进制数据之间的编解码,其支持基础数据类型以及列表、字典。其在iOS端使用了NSJSONSerialization作为序列化的工具,而在Android端则使用了其自定义的JSONUtil.与StringCodec作为序列化工具;StandardMessageCodec
是BasicMessageChannel的默认编解码器,其支持基础数据类型、二进制数据、列表、字典。比JSONMessageCodec更通用。
- 接收/发送消息
接收Dart发来的消息:
// BasicMessageChannel
setMessageHandler(MessageHandler<T> handler)
// MethodChannel
setMethodCallHandler(MethodCallHandler handler)
// EventChannel
void setstreamHandler(EventChannel.StreamHandler handler)
其参数:
// BasicMessageChannel
// var1 是消息内容
// var2 是回复此消息的回调函数
public interface MessageHandler<T> {
void onMessage(T var1, BasicMessageChannel.Reply<T> var2);
}
// MethodChannel
// var1.methed 表示 var1方法名的String
// var1.arguments 表示 var1方法的参数
// var2:提供 var2.success、 var2.error、 var2.notImplemented 三种回复
public interface MethodCallHandler {
void onMethodCall(MethodCall var1,Result var2);
}
// EventChannel
// args 是传递的参数
// eventSink 提供 success、error、endOfStream 三个回调方法
// onCancel 取消监听时调用
public interface StreamHandler {
void onListen(Object args,EventChannel.Eventsink eventsink);
void onCancel(Object o);
}
向dart发送消息:
void send(T message) // dart不回复
void send(T message,BasicMessageChannel.Reply<T> callback) //dart回复
pigeon生成代码
新建flutter_plugin项目,删除flutter_plugin/lib下所有自带代码
pubspec.yaml添加依赖
dev_dependencies:
pigeon: ^16.0.5
- 编写pigeon配置文件,定义通信接口
lib同级目录创建一个pigeons文件夹,新建input_message.dart
文件
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:pigeon/pigeon.dart';
// 输出配置
// https://github.com/flutter/packages/blob/main/packages/pigeon/example/README.md
@ConfigurePigeon(PigeonOptions(
// dart输出位置
dartOut: './lib/pigeon/message.dart',
// Android输出位置
javaOut:
'android/src/main/java/com/example/flutter_plugin/pigeon/Messages.java',
javaOptions: JavaOptions(
// 包名
package: 'com.example.flutter_plugin.pigeon',
),
// // ios输出位置
// // ios/flutter_pigeon_plugin.podspec -> s.source_files = 'Classes/**/*'
// objcHeaderOut: 'ios/Classes/Pigeon.h',
// objcSourceOut: 'ios/Classes/Pigeon.m',
// objcOptions: ObjcOptions(
// // 默认前缀
// prefix: 'FLT',
// ),
))
// 请求参数类型
class SearchRequest {
SearchRequest({required this.query});
String query;
}
// 返回参数类型
class SearchReply {
SearchReply({required this.result});
String result;
}
// flutter 调用 native
// @HostApi():BasicMessageChannel、MethodChannel
// @EventChannelApi():EventChannel
@HostApi()
abstract class FlutterCallNativeApi {
// @async:处理长时间运行的任务或从native异步接收数据
SearchReply search(SearchRequest request);
}
// native 调用 flutter
@FlutterApi()
abstract class NativeCallFlutterApi {
SearchReply query(SearchRequest request);
}
生成代码:
项目目录下运行dart run pigeon --input pigeons/input_message.dart
依赖:(不应添加)
由于该项目作为 Flutter plugin加载,不能识别Android依赖,如果打开Android目录请在build.gradle添加以下依赖(仅作开发使用,妨碍构建)。
android {
def flutterRoot = "C:\\flutter"
dependencies {
compileOnly files("$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar")
compileOnly 'androidx.annotation:annotation:1.9.1'
}
}
- Android端具体实现
编辑Android目录下flutterPlugin.java
package com.example.flutterplugin;
import android.content.Context;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.example.flutterplugin.pigeon.Messages;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
// 1. 继承FlutterCallNativeApi
/** FlutterPigeonPlugin */
public class FlutterPlugin implements FlutterPlugin, Messages.FlutterCallNativeApi {
private Messages.NativeCallFlutterApi nativeApi;
private Context context;
@Override
public void onAttachedToEngine(@NonNull FlutterPlugin.FlutterPluginBinding flutterPluginBinding) {
context = flutterPluginBinding.getApplicationContext();
// 2. setup初始化FlutterCallNativeApi
Messages.FlutterCallNativeApi.setUp(flutterPluginBinding.getBinaryMessenger(), this);
nativeApi = new Messages.NativeCallFlutterApi(flutterPluginBinding.getBinaryMessenger());
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) {
Messages.FlutterCallNativeApi.setUp(binding.getBinaryMessenger(), null);
}
// 3. 自定义具体search方法
// flutter调用native
@Override
public Messages.SearchReply search(Messages.SearchRequest arg) {
Messages.SearchReply reply = new Messages.SearchReply.Builder()
.setResult(arg.getQuery() + "-nativeResult")
.build();
// native调用flutter
nativeApi.query(arg, new Messages.Result<Messages.SearchReply>() {
@Override
public void success(Messages.SearchReply result) {
Toast.makeText(context, result.getResult(), Toast.LENGTH_SHORT).show();
}
@Override
public void error(Throwable error) {
// 处理错误
}
});
return reply;
}
}
- flutter example
添加pubspec.yaml
plugin:
platforms:
android:
package: com.example.flutter_flutterplugin
pluginClass: FlutterPluginPlugin
ios:
pluginClass: FlutterPluginPlugin
linux:
pluginClass: FlutterPluginPlugin
macos:
pluginClass: FlutterPluginPlugin
windows:
pluginClass: FlutterPluginPluginCApi
web:
pluginClass: FlutterPluginWeb
fileName: flutter_plugin_web.dart
flutter调用channel
import 'package:flutter_plugin/pigeon/message.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:developer' as developer;
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
FlutterCallNativeApi? _api;
@override
void initState() {
super.initState();
_initApi();
}
void _initApi() {
try {
_api = FlutterCallNativeApi();
developer.log('API initialized successfully');
} catch (e) {
developer.log('Error initializing API: $e', error: e);
}
}
Future<void> getNativeResult() async {
if (_api == null) {
developer.log('API not initialized');
return;
}
try {
developer.log('Calling native search method');
SearchRequest request = SearchRequest(query: "Zero");
SearchReply reply = await _api!.search(request);
developer.log('Received reply: ${reply.result}');
if (mounted) {
setState(() {
_platformVersion = reply.result;
});
}
} catch (e) {
developer.log('Error calling native method: $e', error: e);
if (mounted) {
setState(() {
_platformVersion = 'Error: $e';
});
}
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: Column(
children: [
Text('Running on: $_platformVersion\n'),
MaterialButton(
height: 40,
color: Colors.blue,
textColor: Colors.white,
elevation: 5,
splashColor: Colors.teal,
padding: const EdgeInsets.all(8),
child: const Text("点击调用 native"),
onPressed: () => getNativeResult())
],
),
),
),
);
}
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
<!-- 应用的包名、版本 -->
package="com.example.app"
android:versionCode="1"
android:versionName="1.0.1">
<!-- 权限设置 --->
<uses-permission android:name="android.permission.INTERNET"/> <!---访问网络>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <!---网络连接是否有效>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <!--读写外部存储器>
<application
<!---参考:https://juejin.cn/post/7006296858494500877-->
android:allowBackup="true" <!---允许app数据备份-->
android:icon="@mipmap/ic_launcher" <!--图标-->
android:label="@string/app_name" <!--标题-->
android:name=".App" <!--应用程序开始的类名-->
android:theme="@style/AppTheme" > <!--主题-->
<activity
android:name=".ui.activities.MainActivity" <!--activity名称-->
android:screenOrientation="portrait" <!--限制此页为竖屏显示-->
android:label="@string/app_name" > <!--标签名称-->
<intent-filter>
<action android:name="android.intent.action.MAIN" /> <!--Main point-->
<category android:name="android.intent.category.LAUNCHER" /> <!--启动时有效-->
</intent-filter>
</activity>
<activity
android:name=".ui.activities.LoginActivity"
android:screenOrientation="portrait"
android:label="@string/app_name" >
</activity>
<service
android:name="com.csr.csrmesh2.MeshService"
android:enabled="true"
android:exported="false" > <!--禁止此服务被其他组件调用和交互-->
</service>
<receiver android:name=".events.ConnectionChangeReceiver"
android:label="ConnectionChangeReceiver">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> <!--网络连接发生变化-->
<!-- <action android:name="android.net.wifi.WIFI_STATE_CHANGED" />-->
</intent-filter>
</receiver>
<!--组件相关配置-->
<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
<meta-data android:name="com.facebook.sdk.ApplicationName" android:value="@string/app_name" />
</application>
</manifest>
常用权限(查看权限大全)
<!--网络-->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!--文件读写-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--录音机-->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!--读取手机状态,获取IMEI -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<!--蓝牙-->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!--相机,及硬件支持-->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<!--接收彩信-->
<uses-permission android:name="android.permission.RECEIVE_MMS" />
<!--访问GMail账户列表-->
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<!--精确位置、粗略位置-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!--通知,Android13以上-->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!--以下不太常用-->
<!--读写日历-->
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<!--读写通讯录-->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<!--读取电话号码-->
<uses-permission android:name="android.permission.READ_PHONE_STATE"
android:maxSdkVersion="29" />
<!--Android11以上-->
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<!--获取传感器(心率等)信息-->
<uses-permission android:name="android.permission.BODY_SENSORS" />
<!--收发短信-->
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<!--悬浮窗、在其他应用上显示-->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
存储权限(Android 10 版本引入了分区存储机制)
XML视图
常见属性
gravity属性值 | 说明 |
---|---|
top | 不改变控件大小,对齐到容器顶部 |
bottom | 不改变控件大小,对齐到容器底部 |
left | 不改变控件大小,对齐到容器左侧 |
right | 不改变控件大小,对齐到容器右侧 |
center_vertical | 不改变控件大小,对齐到容器纵向中央位置 |
center_horizontal | 不改变控件大小,对齐到容器横向中央位置 |
center | 不改变控件大小,对齐到容器中央位置 |
fill_vertical | 若有可能,纵向拉伸以填满容器 |
fill_horizontal | 若有可能,横向拉伸以填满容器 |
fill | 若有可能,纵向横向同时拉伸填满容器 |
特殊属性
常用 API:
FlutterPluginBinding: 向所有 FlutterEngine 注册的插件提供可用的资源。
ActivityPluginBinding : 绑定使ActivityAware插件可以访问关联的Activity和Activity的生命周期方法。
ActivityAware: FlutterPlugin 把 Activity生命周期事件 关联到 FlutterEngine中运行的Activity
FlutterView: 在 Android 设备上显示 Flutter UI
NotificationManager:用于通知用户发生的事件
Resources: 用于访问应用程序资源的类。
调试
VS Code debug
运行前提
- 当前编程语言环境
- 当前编程语言用于调试的扩展,例如Java Extension Pack
- 配置文件launch.json
按钮介绍
继续:跳到下一个断点
单步跳过:跳过当前语句(调用其他文件夹的所有语句),运行下一行
单步调试:进入当前函数内,运行下一行
单步跳出:当debug陷入某个循环时,跳出循环并执行循环外的语句
重启
配置文件
launch.json是用于调试的配置文件,位于.vscode
文件夹,大多数情况下会自动创建,也可以在左侧边栏Run and Debug
点击create a launch.json file
launch.json注释(无需了解)
"configurations": [
{
// 正在调试的项目名称(左侧边栏下拉菜单)
"name": "flutter_project",
// 指定调试模式
// launch模式:支持断点调试
// attach模式:支持对运行中的程序(多为远程服务器)断点调试,点击Add Configuration,选择Attach to Process
"request": "launch",
// 编程语言
"type": "dart"
},
]
断点类型
在代码行左侧断点出右键看到3种类型:
- Add Breakpoint:在断点处阻塞程序
- Add Conditional Breakpoint:在条件为true时断点生效
- Add Logpoint:程序运行中,以非阻塞的方式,记录调试日志
- Inline Breakpoint:当一行代码中有多个函数,光标定位在函数前,Run -> New Breakpoint -> Inline Breakpoint,断点在当前函数之前生效,点击单步调试可以进入当前函数
布局调试
模拟器
开启hyper-v
Windows搜索 -> 启用与关闭Windows功能 -> hyper-v
模拟器报错:找不到libandroid-emu-metrics.dll,重启hyper-v
vscode连接第三方模拟器
项目目录下执行:adb connect 127.0.0.1:[port]
AI画图
Ai 设置:视图->显示网格
多边形变换:多边形工具->自定义变换工具
缩放:鼠标左键向右划放大、向左划缩小
形状合成:选中区域->形状生成器工具->穿过需要的区域->按住Alt穿过不要的区域
渐变:双击圆圈弹出调色板
动画
lottie需要用到Ae,rive是在线编辑。
上架
包名
- Android 是在
android
▸app
▸src
▸main
▸AndroidManifest.xml
- iOS 在
ios
▸Runner
▸Info.plist
应用名称
- Android 是在
android
▸app
▸src
▸main
▸AndroidManifest.xml
中修改android:label="XXX"
; - iOS 在
ios
▸Runner
▸Info.plist
中修改CFBundleName
对应的Value
图标
- Android 在
android
▸app
▸src
▸res
▸mipmap-...
文件夹中替换相应图片 - iOS 在
ios
▸Runner
▸Assets.xcassets
▸AppIcon.appiconset
文件夹中替换相应尺寸的图片, 如果使用不同的文件名,那还必须更新同一目录中的Contents.json
文件。
启动图片
- Android 在
android
▸app
▸src
▸res
▸drawable
▸launch_background.xml
通过自定义drawable来实现自定义启动界面。 - iOS 在
ios
▸Runner
▸Assets.xcassets
▸LaunchImage.imageset
文件夹中替换相应尺寸的图片, 如果使用不同的文件名,那还必须更新同一目录中的Contents.json
文件。
签名(安卓)
- 创建
- android studio方法
以管理员身份运行 android studio -> 打开项目下的Android文件夹 -> Build -> Generate Signed Bundle / APK -> 选择 APK 点击 Next -> Create new
- 命令行ketool方法(推荐)
ketool容易修改密钥,参考Java8版官方文档或更多版本。
使用:
项目下的Android/app文件夹打开cmd
// 创建keystore和密钥对
keytool -genkeypair -alias key -keyalg RSA -keysize 2048 -validity 10000 -keystore ./release.keystore.jks
// 列出可用的证书别名(指定路径)
keytool -list -v -keystore ./release.keystore.jks
// 删除证书(指定别名、路径)
keytool -delete -alias key -keystore ./release.keystore.jks
详细参数
-genkeypair 生成密钥对(公钥和关联的私钥)。将该证书链和私钥存储在由alias标识的新密钥库条目中
-alias 别名,每个keystore都关联这一个独一无二的alias,这个alias通常不区分大小写
-keystore 密钥库位置
-keyalg 指定密钥的算法 (默认值:DSA)
-keysize 指定密钥长度 (默认值取决于keyalg:RSA为2048,DSA为1024)
-validity 指定创建的证书有效期多少天(默认 90)
-storepass 指定密钥库的密码(获取keystore信息所需的密码)
-keypass 指定别名条目的密码(私钥的密码)
-dname 指定证书发行者信息 其中: “CN=名字与姓氏,OU=组织单位名称,O=组织名称,L=城市或区域名 称,ST=州或省份名称,C=单位的两字母国家代码”
-list 显示密钥库中的证书信息
-export 将别名指定的证书导出到文件 keytool -export -alias 需要导出的别名 -keystore 指定keystore -file 指定导出的证书位置及证书名称 -storepass 密码
-file 参数指定导出到文件的文件名
-delete 删除密钥库中某条目 keytool -delete -alias 指定需删除的别 -keystore 指定keystore – storepass 密码
-printcert 查看导出的证书信息 keytool -printcert -file g:\sso\michael.crt
-keypasswd 修改密钥库中指定条目口令 keytool -keypasswd -alias 需修改的别名 -keypass 旧密码 -new 新密码 -storepass keystore密码 -keystore sage
-storepasswd 修改keystore口令 keytool -storepasswd -keystore g:\sso\michael.keystore(需修改口令的keystore) -storepass pwdold(原始密码) -new pwdnew(新密码)
-import 将已签名数字证书导入密钥库 keytool -import -alias 指定导入条目的别名 -keystore 指定keystore -file 需导入的证书
中创建一个默认文件”.keystore”,还会产生一个mykey的别名,mykey中包含用户的公钥、私钥和证书(在没有指定生成位置的情况下,keystore会存在用户系统默认目录)
- 配置
- 为防止 build.gradle 协作时被修改、上线后反编译泄露,把密钥信息保存到 android/local.properties:
storePassword=12345678
keyPassword=12345678
keyAlias=key
storeFile=./release.keystore.jks
local.properties默认不会被添加到Git提交到远程仓库。
- 在android/app/build.gradle添加:
// 读取 local.properties
def mystoreFile = file(localProperties.getProperty('storeFile'))
def mystorePassword = localProperties.getProperty('storePassword')
def mykeyAlias = localProperties.getProperty('keyAlias')
def mykeyPassword = localProperties.getProperty('keyPassword')
// 签名配置
signingConfigs {
release {
keyAlias mykeyAlias
keyPassword mykeyPassword
storeFile mystoreFile
storePassword mystorePassword
}
// // debug模式使用默认的 C:\Users<用户名>.Android\debug.keystore 进行签名,密码是 android
// debug {
// keyAlias 'androiddebugkey'
// keyPassword 'android'
// storeFile file('C:\\Users\\90487\\.android\\debug.keystore')
// storePassword 'android'
// }
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.release
// // 混淆
// minifyEnabled false
// // 混淆文件
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
// debug {
// signingConfig signingConfigs.debug
// // // 包名后添加.debug,可以同时安装多个应用
// // applicationIdSuffix '.debug'
// }
}
android studio验证配置:File -> Project Structure -> Modules -> Signing Configs
参考:官方文档 buildTypes
- 生成签名报告(可以获得md5)
点击右侧Gradle选项卡,android -> app -> Tasks -> android -> signingReport。
如果 Gradle 面板目录中没有 signingReport 文件,进入设置项:File -> Settings -> Experimental -> 取消选中 Only include…during Gradle Sync。然后同步一下:File -> Sync Project with Gradle Files。
编译
- apk
项目根目录下执行
flutter build apk --split-per-abi
- bundles(google 平台需要 bundle 文件格式)
项目根目录下执行
flutter build appbundle
输出 build/app/outputs/bundle/release/app-release.aab
其他
double width = MediaQuery.of(context).size.width; 屏幕宽度
double height = MediaQuery.of(context).size.height; 屏幕高度