发布时间: 更新时间:

安装

java版本控制(jvms)

我们可能需要多个java版本,主要是为了兼容Gradle版本

  1. 下载,解压到想要的路径
  2. 管理员身份打开cmd,cd到在jvms.exe所在的目录下,执行jvms init
  3. jvms.exe rls可列出java版本,如果失败,打开https://site.ip138.com/raw.Githubusercontent.com/查看ip,在C:\Windows\System32\drivers\etc\hosts末尾添加一行[ip] raw.githubusercontent.com
  4. 安装jvms install [version]
  5. jvms.exe ls列出已安装JDK版本
  6. jvms.exe switch [version]切换版本
  7. java -version
  8. 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)

  1. 安装 fvm,命令行运行以下命令:
choco install fvm
  1. 查看当前有哪些版本可用
fvm releases
  1. 安装指定版本
// 有可能缺失文件,建议手动下载
fvm install [version]
  1. 列出所有已安装的 Flutter SDK 版本。
fvm list
  1. 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
  }
}
  1. 切换版本
  • 全局切换:fvm global [version]
  • 项目中切换(每次都要):项目目录下,终端运行fvm use [version],重启vscode

运行上述命令后,项目中创建了一个名为.fvm 的文件夹,文件夹中有 flutter SDK,如果不希望提交此文件夹,在.gitignore文件中添加.fvm/flutter_sdk

  1. 删除某个版本
fvm remove [version]

直接安装

  1. 安装 JDK

下载地址

  1. 安装flutter(windows)

flutter安装

set PUB_HOSTED_URL="https://mirrors.tuna.tsinghua.edu.cn/dart-pub"   #配置国内镜像
set FLUTTER_STORAGE_BASE_URL="https://mirrors.tuna.tsinghua.edu.cn/flutter"
  1. 配置环境变量

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中的图片

使用

  1. app/build.gradle使用library插件(项目下的Android Library已默认使用library插件)
// 更改为library
plugins {
    // id 'com.android.application'
    id 'com.android.library'
}

// 注释掉applicationId
android {
    defaultConfig {
        // applicationId "com.example.myapp"
    }
}
  1. AndroidManifest注释掉application配置

  2. 处理本地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')
}
  1. 生成jar\arr

Android Studio 打开右侧的 Gradle,选择需要打包的module —> Tasks —> build, 双击 assemble

  1. 生成jar\arr到Project\build\library\libs或outputs\aar,移到Project\app\libs

  2. 在模块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:

  1. 在项目中新建File -> New -> New Module -> Java or Kotlin Library

  2. 修改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))
        }
    }
}
  1. 新建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")
    }
}
  1. 将插件发布到本地 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()

其它类型

  • enum  枚举类型,用于表示固定数量的常量值。在switch语句中运行良好。

  • Object 对象类型,允许任何类型。

  • dynamic 动态类型,不仅接受所有对象,而且还允许所有操作

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的值。

方法

实例变量和方法

实例变量:

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语言帮你调用父类的无参数构造函数
  • 代码中显式调用父类的构造函数
  1. 默认调用调用父类的无参数构造函数

如果你没有显式调用父类的构造函数,并且父类有一个无参数构造函数,那么Dart就会帮你在子类的构造函数方法体的最前面,调用父类的无参数构造函数。当然,后面我们会说道,构造函数分成好几部分来初始化成员变量,调用的顺序如下:

  • 初始化列表
  • 父类的无参数构造函数
  • 子类的无参数构造函数

当然,如果父类没有无参数构造函数,或者Dart这种隐式调用无法满足你的要求,那就需要显式调用父类的构造函数了

  1. 显式调用父类构造函数

显式调用父类构造函数,应该在初始化列表中完成

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

Extends是典型的OOP类继承。如果 a类 扩展了 b类,则 a类 可以使用或覆盖 b类 中实现的所有属性、变量、函数 。在 Dart 中,一个类只能扩展一个类。

implements

创建另一个类或接口的实现。当 a类 实现 b类 时,必须实现 b类 中定义的所有功能(所有属性、变量、函数)。一个类可以实现多个接口。

with

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

Isolate 是运行所有 Dart 代码的地方,在许多其他语言里(例如 C++),你可以让多个线程共享相同的内存,但是在 Dart 中,每个线程都有自己的 Isolate 和它自己的内存。

如果你要运行的计算量太过庞大,在 main Isolate 中运行可能会导致丢帧,如果处理可能需要几百毫秒,则考虑创建单独的Isolate,例如:

  • 解码 JSON,HttpRequest 的结果 => compute
  • 加密可能非常耗时
  • 处理图像(例如裁剪)
  • 从 Web 加载图像

事件循环

Dart使用永不阻塞的单线程来处理所有事件。因此,它运行一个事件循环,它从事件队列中取得最先发生的事件,处理它,返回下一个事件进行处理,依此类推,直到事件队列清空为止。

Future

定义

// 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 方法,每种方法都会检查错误返回值的类型。
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 完成之前,指定等待时长,可以创建测试用的模拟网络服务

Streams

每个 future 单一地传递错误,或者数据,Streams随着时间的推移,可以传送零个、多个值,或者是错误。

async await
async 和 await 实际上只是 future 和 streams 的替代语法,在连续的Future中,每一处Future都需要定义、then、return,而且所有内容添加到then下不易阅读,而await只需如下写:

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
Generator
Single valueZero or more values
Sync:intIterable
Async:FutureStream

当您需要延迟生成一系列值时,请考虑使用生成器函数。Dart 内置支持两种生成器函数:

  • 同步生成器:返回一个Iterable对象。
  • 异步生成器:返回一个Stream对象。

实现一个同步生成器函数,将函数体标记为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(...): newWidgetoldWidgetruntimeTypekey同时相等时就会用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.isLoadingwidget.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 

生命周期
在 StatefulWidget 中,使用initStatedispose(),而在 Controller 中我们可以使用相对应的操作:

  1. onStart(开始)[不可覆盖]:组件在内存分配的时间点就会被调用,完成后会调用onInit方法
  2. onInit(初始化):组件在内存分配后会被马上调用,适用于初始化 Controller(例如一些成员属性的初始化),详见Workers小节
  3. onReady(加载完成):在 onInit 一帧后被调用,适合做一些导航进入的事件(例如对话框提示、SnackBar)或异步网络请求
  4. onClose(控制器被释放):在 onDelete 方法前调用、用于销毁 controller 使用的资源(例如关闭事件监听,关闭流对象、动画)或者销毁可能造成内存泄露的对象(例如 TextEditingControllerAniamtionController)。也适用于将数据进行离线持久化。
  5. 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 StatelessWidget。

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

  • 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 是 Fl​​utter 的可定制颜色选择器。可以使用 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支持的数据类型:

DartAndroidios
nullnullnil (NSNull when nested)
booljava.lang.BooleanNSNumber numberWithBool:
intjava.lang.IntegerNSNumber numberWithInt:
int, if 32 bits notenoughjava.lang.LongNSNumber numberWithLong:
doublejava.lang.DoubleNSNumber numberWithDouble:
Stringjava.lang.StringNSString
Uint8Listbyte[]FlutterStandardTypedData typedDataWithBytes:
Int32Listint][]FlutterStandardTypedData typedDataWithInt32:
Int64Listlong[]FlutterStandardTypedData typedDataWithInt64:
Float64Listdouble[]FlutterStandardTypedData typedDataWithFloat64:
Listjava.util.ArrayListNSArray
Mapjava.util.HashMapNSDictionary

Android

Java

  • 访问修饰符
访问范围privatedefaultprotectedpublic
同一类中
同一包中(子类和非子类)×
不同包中的子类××
不同包中的非子类×××

进程和线程

进程

  • 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的事件队列中,可以确保其在当前主线程中执行

参考:HandlerAndroid多线程

生命周期

View的生命周期方法

英文原表

类别方法描述
创建构造函数构造函数有两种形式,一种是在通过代码创建视图时调用的,另一种是在通过布局文件填充视图时调用的。第二种形式解析并应用布局文件中定义的属性。
onFinishInflate()在视图及其所有子视图都从 XML 中映射后调用。
布局onMeasure(int, int)调用来确定此视图及其所有子视图的尺寸要求。
onLayout(boolean, int, int, int, int)当此视图必须为其所有子视图分配大小和位置时调用。
onSizeChanged(int, int, int, int)当此视图的大小改变时调用。
绘画onDraw(Canvas)当视图必须呈现其内容时调用。
事件处理onKeyDown(int, KeyEvent)当发生按键事件时调用。
onKeyUp(int, KeyEvent)当发生按键释放事件时调用。
onTrackballEvent(MotionEvent)当轨迹球运动事件发生时调用。
onTouchEvent(MotionEvent)当发生触摸屏运动事件时调用。
焦点onFocusChanged(boolean, int, Rect)当视图获得或失去焦点时调用。
onWindowFocusChanged(boolean)当包含视图的窗口获得或失去焦点时调用。
附加onAttachedToWindow()当视图附加到窗口时调用。
onDetachedFromWindow()当视图与其窗口分离时调用。
onWindowVisibilityChanged(int)当包含视图的窗口的可见性发生改变时调用。

Activity生命周期

Activity的启动

  1. Activity 调用onCreate方法,将资源 ID R.layout.main_activity 更改为 setContentView()
  2. Activity 调用onWindowAttributesChanged 方法,而且这个方法连续调用多次
  3. View 调用构造方法
  4. View 调用onFinishInflate方法,说明这个时候View已经填充完毕,但是还没开始触发绘制过程
  5. Activity 调用onstart方法, 进入“已启动”状态
  6. Activity 再次调用 onWindowAttributesChanged 方法
  7. Activity 调用onResume,“已恢复”状态,进行后面初始化步骤,并进入与用户互动的状态
  8. Activity 调用onAttachedToWindow,Activity跟Window进行绑定
  9. View 调用onAttachedToWindow,View跟Window进行绑定
  10. View 调用 onWindowVisibilityChanged(int visibility),参数变为 View.VISIABLE
  11. View 调用onMeasure,开始测量
  12. View 调用onSizeChanged,表示测量完成,尺寸发生了变化
  13. View 调用onLayout,开始摆放位置
  14. View 调用 onDraw,开始绘制
  15. Activity 调用onWindowFocusChanged(boolean hasFocus),此时为true,代表窗体已经获取了焦点
  16. View 调用 onWindowFocusChanged(boolean hasWindowFocus),此时为true,代表当前的控件获取了Window焦点,当调用这个方法后说明当前Activity中的View才是真正的可见了

Activity的退出

  1. Activity 调用 onPause,中断时的暂停状态
  2. View 调用 onWindowVisibilityChanged(int visibility),参数变为 View.GONE,View中对应的Window隐藏
  3. Activity 调用onWindowFocusChanged(boolean hasFocus),此时为false,说明Actvity所在的Window已经失去焦点
  4. Activity 调用 onStop,此时Activity已经切换到后台
  5. Activity 调用 onDestory,此时Activity开始准备销毁,实际上并不代表Activity已经销毁
  6. View 调用 onDetachedFromWindow,此时View 与Window解除绑定
  7. Activity 调用 onDetathedFromWindow ,此时Activity 与Window 解除绑定
  8. View即将被销毁,可以在 onDetachedFromWindow 方法中做一些资源释放,防止内存泄漏

android四大组件

activity

  • 每个 Activity 提供一个用户界面窗口,一个app 由多个Activity 组成,其中一个主 Activity为程序入口。

    • 应用窗口(TYPE_APPLICATION):Activity 的默认窗口,层级最低。
    • 子窗口(TYPE_APPLICATION_PANEL):依附于应用窗口(如 Dialog)。
    • 系统窗口(TYPE_TOAST):无需 Activity 承载,直接由系统管理(如 Toast、悬浮窗)。
  • Activity通过 IntentBundle 实现数据传递。

  • 每一个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

  1. 创建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更通用。
  1. 接收/发送消息

接收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生成代码

  1. 新建flutter_plugin项目,删除flutter_plugin/lib下所有自带代码

  2. pubspec.yaml添加依赖

dev_dependencies:
  pigeon: ^16.0.5
  1. 编写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'
    }
}    
  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;
  }
}
  1. 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 是在 androidappsrcmainAndroidManifest.xml
  • iOS 在 iosRunnerInfo.plist

应用名称

  • Android 是在 androidappsrcmainAndroidManifest.xml 中修改android:label="XXX";
  • iOS 在 iosRunnerInfo.plist 中修改CFBundleName对应的Value

图标

  • Android 在androidappsrcresmipmap-... 文件夹中替换相应图片
  • iOS 在 iosRunnerAssets.xcassetsAppIcon.appiconset文件夹中替换相应尺寸的图片, 如果使用不同的文件名,那还必须更新同一目录中的Contents.json文件。

启动图片

  • Android 在androidappsrcresdrawablelaunch_background.xml 通过自定义drawable来实现自定义启动界面。
  • iOS 在 iosRunnerAssets.xcassetsLaunchImage.imageset文件夹中替换相应尺寸的图片, 如果使用不同的文件名,那还必须更新同一目录中的Contents.json文件。

签名(安卓)

  1. 创建
  • 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会存在用户系统默认目录)

  1. 配置
  • 为防止 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

  1. 生成签名报告(可以获得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; 屏幕高度

Licensed under CC BY-NC-SA 4.0
最后更新于 2024-11-30