第2章:Shell高级加密可执行

[TOC]

Shell高级加密可执行

shc是一个加密shell脚本的工具.它的作用是把shell脚本转换为一个可执行的二进制文件.

1
brew install shc

使用方法:

1
shc -r -f script-name #注意:要有-r选项, -f 后跟要加密的脚本名.

运行后会生成两个文件,

1
2
3
4
script-name.x是加密后的可执行的二进制文件. (双加或者 ./script-name 即可运行.)


script-name.x.c是生成script-name.x的原文件(c语言)

shc -v -r -f echo1 打包成echo1.x二进制文件,同时生成 echo1.x.c文件,这个为C语言文件。猜想这个工具是将shell脚本转为C语言,然后在打包成二进制文件。

第2章:Shell高级技巧

[TOC]

Shell高级技巧

1
2
3
4
5
# [shell替换和去掉换行符](http://www.noobyard.com/article/p-ahlemikj-nz.html)
featureBranceNamesString=$(echo ${featureBranceNamesString} |sed 's/ /\n/g'|awk '{{printf"%s",$0}}')

# [Shell 命令变量去除空格方法](https://blog.csdn.net/jjc120074203/article/details/126663391)
FileContent=${FileContent// /}

其他问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# [Mac环境下shell脚本中的map](https://www.jianshu.com/p/a55480b793b0)
# declare -A myMap=(["my00"]="00" ["my01"]="01")
# myMap["my02"]="02"
# myMap["my03"]="03"
# declare -A targetBranchConfig_realMap=()
# targetBranchConfig_realMap["uploadChannelShortcut"]="${packagePgyerChannelShortcutResult_upload}"
# targetBranchConfig_realMap["uploadChannelKey"]="${packagePgyerChannelKeyResult_upload}"

uploadChannelShortcut_key="uploadChannelShortcut"
uploadChannelKey_key="uploadChannelKey"
targetBranchConfig_realMap="{"
targetBranchConfig_realMap+="\"${uploadChannelShortcut_key}\":\"${packagePgyerChannelShortcutResult_upload}\", \"${uploadChannelKey_key}\":\"${packagePgyerChannelKeyResult_upload}\""
if [ -z "${packagePgyerChannelShortcutResult_download}" ] || [ "${packagePgyerChannelShortcutResult_download}" == "null" ]; then
packagePgyerChannelShortcutResult_download=${packagePgyerChannelShortcutResult_upload}
packagePgyerChannelKeyResult_download=${packagePgyerChannelKeyResult_upload}
fi
targetBranchConfig_realMap+=","
targetBranchConfig_realMap+="\"downloadChannelShortcut\":\"${packagePgyerChannelShortcutResult_download}\", \"downloadChannelKey\":\"${packagePgyerChannelKeyResult_download}\""
targetBranchConfig_realMap+="}"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#获取键值对的个数,或者数组长度:
cat json.txt | jq '.[0].employees|length'

# 取键foo的值:
echo '{"foo": 42, "bar": "less interesting data"}' | jq '.foo'
42
# 获取json所有键:
echo '{"foo": 42, "bar": "less interesting data"}' | jq 'keys'
[
"bar",
"foo"
]
# has判断是否存在某个key
echo '{"foo": 42, "bar": "less interesting data"}' | jq 'has("foo")'
true
# 获取键值对的个数,或者数组长度
echo '{"foo": 42, "bar": "less interesting data"}' | jq 'length'

一、主要用法

删除所有的空格

1
2
3
4
buildContainBranchsString=(`echo ${buildContainBranchsString} | sed s/[[:space:]]//g`) # 删除所有的空格,修复所填分支信息有空格

#echo "buildContainBranchsString=${buildContainBranchsString}"
buildContainBranchArray=(`echo ${buildContainBranchsString} | tr '#' ' '`) # 字符串拆分成数组

字符串转换数组

1
2
3
4
nocodeBranceNamesString=$(cat ${FILE_PATH} | ${JQ_EXEC} '.nocode_brances' | ${JQ_EXEC} '.[].name')
echo ${nocodeBranceNamesString}

nocodeBranceNamesArray=(${nocodeBranceNamesString//,/}) # 字符串转数组

shell 使用jq解析json字符串数组

如何在shell中使用jq cmd变量

1
2
3
#    branchMapArray=$(echo ${branchRootMap} | ${JQ_EXEC} -r '.online_brances') # -r 去除字符串引号
# [如何在shell中使用jq cmd变量](https://www.5axxw.com/questions/content/5ucfc4)
branchMapArray=$(echo ${branchRootMap} | jq -r --arg branchsKey "$branchsKey" '.[$branchsKey]')
1
brew install shc
1
2
3
shc -r -f hello_script.sh

shc -r -f hello_script.sh -o hello_script
1
2
3
4
5
6
7
8
9
10
11
12
shell 单括号运算符号:
a=$(date);
等同于:
a=date;



双括号运算符:
a=$((1+2));
echo $a;
等同于:
a=expr 1 + 2

删除

1
2
3
4
5
6
7
8
#删除所有的空格
sed s/[[:space:]]//g

#删除行末空格
sed 's/[ \t]*$//g'

#删除行首空格
sed 's/^[ \t]*//g'

shell中使用jq命令修改json文件(合并,修改等)

  1. 目标

文件package.json

1
2
3
4
5
6
7
8
9
10
{
"menus": {
"commandPalette": [
{
"command": "go.test.refresh",
"when": "false"
}
]
}
}

文件add.json

1
2
3
4
5
6
7
8
9
10
11
12
{
"add_menu": [
{
"command": "go.explorer.refresh",
"when": "false"
},
{
"command": "go.explorer.open",
"when": "false"
}
]
}

希望把add.json文件里的add_menu合并到package.json的 menus.commandPalette中, 得到一下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"menus": {
"commandPalette": [
{
"command": "go.test.refresh",
"when": "false"
},
{
"command": "go.explorer.refresh",
"when": "false"
},
{
"command": "go.explorer.open",
"when": "false"
}
]
}
}

2.上脚本

1
jq -s '.[0].menus.commandPalette = .[0].menus.commandPalette + .[1].add_menu | .[0]' ./package.json ./add.json

3.解释

参数-s

表示读取多个json对象, 放入一个数组里在一起出来, 开始的.表示整个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ jq -s '.' ./package.json ./add.json
[
{
"menus": {
"commandPalette": [
...
]
}
},
{
"add_menu": [
...
]
}
]

定位取值

数组使用中括号加索引读取等读取, 如 [0] [1], 原命令里的.[0]和.[1]就分别是表示package.json和info.json里的json对象, 其中的.则是表示合并后数组.

要获取json更深层级的对象时, key之间用点号拼接即可. 如

1
2
$ jq '.add_menu[0].command' ./add.json 
"go.explorer.refresh"

管道|

基本和shell里的管道概念是一致的, 管道左侧命令的输出, 将变成管道右侧命令的输入. 如原命令里, .[0].menus.commandPalette = .[0].menus.commandPalette + .[1].add_menu | .[0] 就有一个管道, 左侧的命令是修改合并后的数组中第一个单元的某些值, 然后输出整个数组(包含两个单元, 有package.json和info.json的信息), 然后管道右侧命令接收到整个数组后, 只筛选出第一个单元进行输出

加法

  • 针对数组, 是拼接效果(如原命令)
  • 针对数字, 则是正常加法
  • 针对{}对象, 合并, 相同key覆盖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 数组加法
$ echo '[1,2] [3,4]'|jq -s '.[0] + .[1]'
[
1,
2,
3,
4
]

# 数字加法
$ echo '1 2'|jq -s '.[0] + .[1]'
3

# {}对象, 相同key后面覆盖前面
$ echo '{"a":1, "b":2} {"b":21, "c":3}'|jq -s '.[0] + .[1]'
{
"a": 1,
"b": 21,
"c": 3
}

4.总结

从某个数据源读取一些json信息, 并以此针对某个json的制定位置进行修改, 基本思路就是使用-s参数, 先把所有数据合并为一个大数组, 然后就行修改, 过后通过管道筛选出需要的数据.

jq是一个非常强悍的处理json的命令行工具, 完全可以把它当做一门语言来学. 我也正在学习当中, 你若有jq相关需求解决不了, 我很乐意帮忙研究解决.

二、超时

MacOS超时指令-gtimeout

在linux下面可以使用timeout命令设置命令的超时时间,macos下同样也有,只不过不是timeout而是gtimeout命令

安装

1
brew install coreutils

使用

1
gtimeout 10 sh ./demo.sh # 指令脚本,10秒后超时退出

也可以使用别名,将gtimeout设置为timeout

1
alias timeout=gtimeout

第1章:Shell入门

[TOC]

Shell入门

基础知识

  • shell数组

    MentionedList=(“zhangsan” “lisi”)

一、主要用法

1、获取值

1
2


2、修改值

2.1、修改文件名

1、传参并修改app_info.json文件内容

如修改以下app_info.json文件内容

1
2
3
4
5
{
"version": 0,
"time": "unknow time",
"brance": "unknow brance",
}

修改命令如下:

1
#sed -i '' 's/unknow brance/dev_1.0.0_fix/g' app_info.json

如果要使用传入的参数,则命令如下:

1
2
3
4
5
# 使用传入的参数方法1:把单引号改为双引号(因为单引号会阻止转义)
sed -i '' "s/unknow brance/$1/g" app_info.json

# 使用传入的参数方法2:变量${}外的字符串全都用单引号''圈住
sed -i '' 's/unknow brance/'$1'/g' app_info.json

update_app_info.sh脚本的核心命令:

1
2
3
4
5
6
7
8
# update_app_info.sh
# app信息中加入打包的分支名称,便于知晓当前包的来源分支
BranceName=$1
echo "BranceName=$BranceName"
sed -i '' "s/unknow brance/$BranceName/g" app_info.json

# 将含信息的文件拷贝到app中,以便在app可以获取这些信息
cp app_info.json ../wish/asset/data

update_app_info.sh脚本的完整命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
FullBranceName=$1
ShortBranceName=${FullBranceName##*/}
echo "ShortBranceName=$ShortBranceName"
sed -i '' 's/unknow brance/'${ShortBranceName}'/g' app_info.json

#cp app_info.json ../flutter_updateversion_kit/assets/data
app_info_dir_home="../flutter_updateversion_kit/assets"
if [ ! -d "$app_info_dir_home" ];then
mkdir $app_info_dir_home
#echo "assets文件夹创建成功"
#else
#echo "assets文件夹已经存在"
fi


app_info_dir="$app_info_dir_home/data"
if [ ! -d "$app_info_dir" ];then
mkdir $app_info_dir
#echo "assets/data文件夹创建成功"
#else
#echo "assets/data文件夹已经存在"
fi

fileName="app_info.json"
app_info_file_path=$app_info_dir/$fileNam
cp $fileName $app_info_file_path

jenkins中,直接取定义分支名时候的名称作为变量:

1
2
cd $WORKSPACE/bulidScript
sh update_app_info.sh $BRANCH

2、读取Json及其属性值

假设json文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"version": "1.0.0",
"package_create_time": "package unknow time",
"package_from_brance": "package unknow brance",
"package_default_env": "package unknow env",
"brances_record_time": "03.18",
"feature_brances": [
{
"name": "dev_1.0.0_fix",
"des": "线上版问题修复或优化"
}
]
}

前提:

必须安装jq,安装方法如下:

mac上安装brew后,执行brew install jq安装jq

2.1、如果是json文件

则读取 version、package_create_time、feature_brances.name 的值分别如下:

1
2
3
4
5
6
7
8
9
10
#! /bin/bash
JQ_EXEC=`which jq`
FILE_PATH=app_info.json

packageVersion=$(cat $FILE_PATH | ${JQ_EXEC} .version | sed 's/\"//g')
packageCreateTime=$(cat $FILE_PATH | ${JQ_EXEC} .package_create_time | sed 's/\"//g')
echo "packageVersion=$packageVersion,packageCreateTime=$packageCreateTime"

featureBranceName=$(cat $FILE_PATH | ${JQ_EXEC} .feature_brances.name | sed 's/\"//g')
echo "featureBranceName=$featureBranceName"

2.2、如果是json数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
responseResult=$(\
curl $ROBOTURL \
-H 'Content-Type: application/json' \
-d "
{
\"msgtype\": \"text\",
\"text\": {
\"content\": \"${LastNotificationText}\"
\"mentioned_list\":$MentionedList
}
}"
)

responseResultCode=$(**echo** ${responseResult} | jq '.errcode') # mac上安装brew后,执行brew install jq安装jq

Shell脚本8种字符串截取方法总结

一、shell 中的与、表达式

  • 逻辑与的表达:
1
2
3
4
1)、if [ $xxx=a -a $xx=b ]

注:-a表示and的意思
2)、if [ $xxx=a ] && [ $xx=b ]
  • 逻辑或的表达:
1
2
3
4
1)、if [ $xxx=a -o $xx=b ]

注:-o表示or的意思
2)、if [ $xxx=a ] || [ $xx=b ]

一、Shell的特殊变量

变量 含义
$0 当前脚本的文件名
$# 传递给脚本或函数的参数个数。
$n 传递给脚本或函数的第n个参数
$* 传递给脚本或函数的所有参数。
$@ 传递给脚本或函数的所有参数。被双引号(“ “)包含时,与 $* 稍有不同,下面将会讲到。
$? 上个命令的退出状态,或函数的返回值。
$$ 当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID。

1、 \$* 和 \$@ 的区别

\$* 和 \$@ 都表示传递给函数或脚本的所有参数

情况①:不被双引号(“ “)包含时,都以”\$1” “\$2” … “\$n” 的形式输出所有参数。

情况②:但是当它们被双引号(“ “)包含时,”\$*” 会将所有的参数作为一个整体,以”\$1 \$2 … \$n”的形式输出所有参数;”\$@” 会将各个参数分开,以”\$1” “\$2” … “\$n” 的形式输出所有参数。

2、shell中的重定向 1>&2

二、echo

1、超级终端的字体背景和颜色显示

格式: echo -e “\033[字背景颜色;字体颜色m字符串\033[0m”

例如:
echo -e “\033[41;36m something here \033[0m”

其中41的位置代表底色, 36的位置是代表字的颜色

那些ascii code 是对颜色调用的始末.
\033[ ; m …… \033[0m

参考:

https://blog.csdn.net/panpan639944806/article/details/23930553

https://blog.csdn.net/u014470361/article/details/81512330

颜色 字色值 背景色值
黑色 30 40
紅色 31 41
綠色 32 42
黃色 33 43
藍色 34 44
紫紅色 35 45
青藍色 36 46
白色 37 47

三、判断语句

1、对字符串的判断

判断参数个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if [[ -z $2 ]]
then
echo 'Error:参数数量过少,请重新输入'
exit_script
fi
if [[ -n $3 ]]
then
echo 'Error:参数数量过多,请重新输入'
exit_script
fi
if [ -z ${xcarchive_file_path} ] \
|| [ -z ${APPENVIRONMENT} ];
then
echo 'Error:上述2个参数都不能为空'
echo '请重新执行:sh base_exportArchive.sh ${xcarchive_file_path} ${APPENVIRONMENT}'
exit_script
fi

1.1、截取字符串

注意赋值等号两边不能有空格

注意赋值等号两边不能有空格

注意赋值等号两边不能有空格

1.1.1、根据路径取目录和文件名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
file_path=d/dir1/dir2/file.png
file_dir=${file_path%/*}
file_name=${file_path##*/}
echo "file_dir = ${file_dir}"
echo "file_name = ${file_name}"
exit


APPPROJECT_NAME=${workspace_file_path##*/}


xcarchive_file_path=./aaa/bbb/ccc/App1Enterprise.xcarchive
xcarchive_file_dir=${xcarchive_file_path%/*} # 文件目录
xcarchive_file_name_withSuffix=${xcarchive_file_path##*/} # 文件主名+文件后缀名
xcarchive_file_name=${xcarchive_file_name_withSuffix%.*} # 文件主名
xcarchive_file_suffix=${xcarchive_file_name_withSuffix##*.}# 文件后缀名
echo "xcarchive_file_dir: ${xcarchive_file_dir}"
echo "xcarchive_file_name: ${xcarchive_file_name}"
echo "xcarchive_file_suffix: ${xcarchive_file_suffix}"
1.1.2、根据1.0.0+100去版本号和编译号
1
2
3
4
5
6
version=1.0.0+100
app_bundle_version=${version%+*} # 取+号左侧,结果1.0.0()
app_build_version=${version#*+} # 取+号右侧,结果100(注意赋值等号两边不能有空格)
echo "app_bundle_version = ${app_bundle_version}"
echo "app_build_version = ${app_build_version}"
exit

1、环境判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
APPENVIRONMENT=$2           # app的打包环境
echo "app的打包环境APPENVIRONMENT: ${APPENVIRONMENT}"


CUR_DIR=$PWD #$PWD代表获取当前路径,当cd后,$PWD也会跟着更新到新的cd路径。这个和在终端操作是一样的道理的
if [ $APPENVIRONMENT == "Product" ]; # 生产环境
then
ExportOptionsPlist_PATH=$CUR_DIR/ExportOptions_inhouse.plist
elif [ $APPENVIRONMENT == "PreProduct" ]; # 预生产环境
then
ExportOptionsPlist_PATH=$CUR_DIR/ExportOptions_adhoc.plist
elif [ $APPENVIRONMENT == "Develop1" -o $APPENVIRONMENT == "Develop2" -o $APPENVIRONMENT == "Develop3" ];
then # 测试环境
ExportOptionsPlist_PATH=$CUR_DIR/ExportOptions_dev.plist
else # 不是允许/支持的环境
echo "-------- Error:所执行的脚本命令中,输入的${APPENVIRONMENT}不是允许/支持的环境,故不再继续执行 --------"
exit
fi

1、判断字符串是否以某个字符串结尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 判断字符串是否以某个字符串结尾
endWithSuffix() {
fullString=$1
suffixString=$2
echo "判断 ${fullString} 是否以 ${suffixString} 结尾"
file_name_withSuffix=${fullString##*/} # 文件主名+文件后缀名
#file_name=${file_name_withSuffix%.*} # 文件主名
file_suffix=${file_name_withSuffix##*.} # 文件后缀名
#echo "file_name: ${file_name}"
echo "file_suffix: ${file_suffix}"
echo "suffixString: ${suffixString}"
if [ ${file_suffix} == ${suffixString} ]; then
echo 'suffix correct'
return 1
else
echo 'suffix error'
return 0
fi
}
# 使用示例:
# endWithSuffix "./aaa/bbb/ccc/App1Enterprise.xcarchive" 'xcarchive'
endWithSuffix "./aaa/bbb/ccc/App1Enterprise.ipa" 'xcarchive'
if [ $? == 0 ]; then
echo '执行失败:文件后缀错误,请重新输入'
exit
fi
exit

1.1、判断是不是以http开头

if [[$1 =~^http.* ]]; then
commond
else
commond
fi

1、shell中[[]]和[]的主要区别

  • 实际上是bash 中 test 命令的简写。即所有的 [ expr ] 等于 test expr
  • [[ expr ]] 是bash中真正的条件判断语句

2、对文件名filename的判断

1、获取当前文件路径

1
2
curPath=$(cd "$(dirname "$0")"; pwd) # 获取当前文件路径
echo "curPath=${curPath}"
表达式 含义
-e filename 如果 filename存在,则为真
-d filename 如果 filename为目录,则为真
-f filename 如果 filename为常规文件,则为真
-L filename 如果 filename为符号链接,则为真
-h filename 如果文件是软链接,则为真

使用举例:

1
2
3
4
5
6
7
8
9
10
11
AssertExists() {
if [[ ! -e "$1" ]]; then
if [[ -h "$1" ]]; then
EchoError "The path $1 is a symlink to a path that does not exist"
else
EchoError "The path $1 does not exist"
fi
exit -1
fi
return 0
}

其他表达式:

表达式 含义
-r filename 如果 filename可读,则为真
-w filename 如果 filename可写,则为真
-x filename 如果 filename可执行,则为真
-s filename 如果文件长度不为0,则为真
filename1 -nt filename2 如果 filename1比 filename2新,则为真。
filename1 -ot filename2 如果 filename1比 filename2旧,则为真。

3、对字符串str的判断

表达式 含义
if [ str1 = str2 ] 当两个串有相同内容、长度时为真
if [ str1 != str2 ] 当串str1和str2不等时为真
if [ -n str1 ] 当串的长度大于0时为真(串非空)
if [ -z str1 ] 当串的长度为0时为真(空串)
if [ str1 ] 当串str1为非空时为真

shell 中利用 -n 来判定字符串非空。

使用示例:

1
2
3
4
5
6
7
8
9
RunCommand() {
if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
echo "♦ $*"
fi
"$@"
return $?
}

# 如果$VERBOSE_SCRIPT_LOGGING值不为空,则通过"$*"打印传递给脚本或函数的所有参数(将所有的参数作为一个整体输出),如果为空,则通过"$@"传递给脚本或函数的所有参数(将所有的参数依次分开输出),并通过$?返回上个命令的退出状态,或函数的返回值。

注意

  • 使用[]和[[]]的时候不要吝啬空格,每一项两边都要有空格,[[ 1 == 2 ]]的结果为“假”,但[[ 1==2 ]]的结果为“真”!

判断

1、数组里是否包含某个元素

1
2
3
4
5
6
7
8
noBranchNames=("HEAD" "->")
if [[ "${noBranchNames[*]}" =~ ${devBranchName} ]]; then
#echo "devBranceMapString=${devBranceMapString}"
packageMergerBranchString+="${devBranceName}:${devBranceDes}#"
else
echo ${PackageErrorCode}:${PackageErrorMessage}
exit_script
fi

文件包含/公用代码

  • 权威知识请查看:

    Shell 文件包含

    testReturn1.sh

    1
    2
    > OUTPUT_ARCHIVE_PATH="/User/xxx/output/abc"
    >

    d

    1
    2
    3
    4
    5
    6
    > #!/bin/bash
    > . ./testReturn1.sh
    > # source ./testReturn1.sh # 此种方式也可以
    > # sh ./testReturn1.sh # 此种方式无法拿到另一个shell中的变量
    > echo "OUTPUT_ARCHIVE_PATH=${OUTPUT_ARCHIVE_PATH}"
    >

文件操作

1、mkdir -p中的 p有什么用

-p, –parents 需要时创建上层目录,如目录早已存在则不当作错误

命令格式:mkdir [-p] DirName

说明:建立一个子目录。

参数:-p 确保目录名称存在,如果目录不存在的就新创建一个。

1
2


获取当前文件所在的目录

1
2
3
4
5
# 注意:${0} 指的是执行这个方法的那个文件的所在目录。
# 所以当外部不是执行脚本,而只是引用脚本,即使用如 source ./xx.sh 的时候,所以无法获取我们真正想要的当前文件的所在目录
curdir=$(cd $(dirname $0); pwd)
echo "脚本$0当前文件所在的目录:${curdir}"
exit

第4节:详解Future

Flutter学习之三方库,网络库,轮播图库,下拉刷新库,跳转传值库,BaseWidght封装

Flutter的网络请求库:Dio

RefreshIndicator是Material风格的下拉刷新组件。

CupertinoSliverRefreshControl 是ios风格的下拉刷新控件。

参考文章:

1
2
3
4
5
import 'package:dio/dio.dart';

Future<String> getData() async {
return "123";
}

Flutter中的网络请求
网络请求是非常典型的异步任务,下面我们就来结合网络请求来看看Flutter中的异步是如何使用的。

网络请求的方式有很多,这里我就直接用目前比较好用的DIO网络请求库 了,你也可以使用官方文档中的网络请求 ,都是可以的。

下面我们来简单用一用网络请求。

这里我使用的聚合上的一个接口

接口地址:http://v.juhe.cn/toutiao/index?type=keji&key=4c52313fc9247e5b4176aed5ddd56ad7

关于DIO如何使用这里就不讲了,Github上文档很详细,使用起来也很简单。
下面我们直接用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'package:dio/dio.dart'; // 导包

/// 请求接口获取数据的方法
Future<Response> getData() async {
String url = "http://v.juhe.cn/toutiao/index";
String key = "4c52313fc9247e5b4176aed5ddd56ad7";
String type = "keji";

print("开始请求数据");
Response response = await Dio().get(url, queryParameters: {"type": type, "key": key});
print("请求完成");

return response;
}

注意一下几点:

网络请求是耗时操作
要使用async来标明getData这个函数是一个异步函数
await 用于等待请求返回的结果,此时会阻塞掉后面的代码,只有当请求结束后面的代码才会执行
async标注的函数其返回值类型是Future
然后我们就可以在main函数中来接收网络请求后的结果了:

1
2
3
4
5
6
7
8
9
10
11
main() {
getData().then((result) {
print("接口返回的数据是:${result}");
}).whenComplete((){
print("异步任务处理完成");
}).catchError((){
print("出现异常了");
});

print("我是在请求数据后面的代码呦!");
}

我们来看看执行的结果:

这样一来,我们就完成了Flutter中的异步操作了,可以看到,相对于原生Android来讲,Flutter中的异步是非常简单的。

Flutter 请求网络数据时显示加载中
一个很常见的需求,在首次进入页面时,此时数据还需要从网络上获取,我们希望在网络请求完成之前显示一个加载页面,请求完成之后再显示数据。此时,我们就可以使用FutureBuilder来完成了

首先是接口请求函数,为了更明显的能看到加载控件的显示,这里的异步请求函数中我延时3秒后再请求数据,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_async/widget/loading.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '新闻列表',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: '新闻列表'),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: FutureBuilder(
future: _getNews(),
builder: (BuildContext context, AsyncSnapshot<Response> snapshot) {
/*表示数据成功返回*/
if (snapshot.hasData) {
Response response = snapshot.data;
return Text("${response.data.toString()}");
} else {
return LoadingWidget();
}
},
));
}
}


/**

* 请求接口获取数据
*/
Future<Response> _getNews() async {
await Future.delayed(Duration(seconds: 3), () {
print("延时三秒后请求数据");
});

String url = "http://v.juhe.cn/toutiao/index";
String key = "4c52313fc9247e5b4176aed5ddd56ad7";
String type = "keji";

print("开始请求数据");
Response response =
await Dio().get(url, queryParameters: {"type": type, "key": key});

print("请求完成");

return response;
}

运行效果:
在这里插入图片描述

下面是demo,需要的可以下载:
flutter_async Demo

第3节:详解Platform Channel

参考文章:

一、基础知识

1、Channel

Flutter定义了三种不同类型的Channel,分别如下:

Channel类型 用途
BasicMessageChannel 用于传递字符串和半结构化的信息
MethodChannel 用于传递方法调用(method invocation)
EventChannel 用于数据流(event streams)的通信
  • MethodChannel传递的数据支持什么类型?
  • Dart数据类型与Android,iOS类型的对应关系是怎样的?

这两个问题的答案同样来自官方文档:

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 not enough 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
Class Note
BinaryCodec 二进制数据类型,泛型对应 ByteBuffer,传递 byte 数组时使用
JSONMessageCodec Json消息类型,泛型对应 Object。传递 Json时可使用
StringCodec 字符串消息类型,泛型对应 String。传递字符串
StandardMessageCodec 标准消息类型,泛型对应 Object。传递 Map 等的时候可使用此类型

第1节:详解布局

参考文章:

一、基础知识

1.spaceEvently

一行中如果有2个widget(五个五角星暂且看成1个widget),那么除去2widget所占据的宽度,3个红色方块宽度是一样的, 如下图所示

img

2.spaceBetween

一行中如果有2个widget,那么分布2端,如下图所示

img

3.spaceAround

一行中如果有2个widget,那么每个widget左右都是相同的方块,如下图所示

img

类型 含义 row举例 column举例
start
(default)
从行首开始排列。每行第一个弹性元素与行首对齐,同时所有后续的弹性元素与前一个对齐 在这里插入图片描述 在这里插入图片描述
start
textDirection: TextDirection.rtl,
textDirection的值为从右至左 row_start-rtl 在这里插入图片描述
center 伸缩元素向每行中点排列。每行第一个元素到行首的距离将与每行最后一个元素到行尾的距离相同。 在这里插入图片描述 在这里插入图片描述
end 从行尾开始排列。每行最后一个弹性元素与行尾对齐,其他元素将与后一个对齐。 在这里插入图片描述 在这里插入图片描述
space-between 在每行上均匀分配弹性元素。相邻元素间距离相同。每行第一个元素与行首对齐,每行最后一个元素与行尾对齐。 在这里插入图片描述 在这里插入图片描述
space-around 在每行上均匀分配弹性元素。相邻元素间距离相同。每行第一个元素到行首的距离和每行最后一个元素到行尾的距离将会是相邻元素之间距离的一半。 在这里插入图片描述 在这里插入图片描述
spaceEvenly 将主轴空白区域均分,使各个子控件间距相等 在这里插入图片描述 在这里插入图片描述

expand 要再column或row中使用

层叠

4.6 层叠布局 Stack、Positioned

eg:在视图上,显示loading动画

问:flutter text 左对齐_如何在Flutter中将文本垂直和水平居中?

答:首先可以使用Center让控件居中,其次使用textAlign设置水平居中,示例代码如下:

1
2
3
4
5
6
child: Center(
child: Text(
"Hello World",
textAlign: TextAlign.center,
),
),

flutter 隐藏显示控件

1
2
3
4
Offstage(
offstage: true, //true隐藏,false显示
child: '子控件',
),

第7节:详解Animation-2动画组

[TOC]

详解Animation-2动画组

参考文章:

一、Animation动画组-串行

参考文档:

串行动画的实现方案总共有三种,分别是 监听状态法, Interval时间间隔法, TweenSequence动画序列法.

1、监听状态法

状态监听法主要通过AnimationController监听动画的completed状态,然后再去执行下一个动画,如此往复,直到所有动画完成.

例如现在我们需要实现先执行组件在0.3秒钟往下偏移50个单位,然后再执行在0.6s中组件的颜色由 橘色 变为 红色.

整体Demo代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
class FlutterAnimationWidget extends StatefulWidget {
TurnTableLotteryPage({Key key}) : super(key: key);

@override
_FlutterAnimationWidgetState createState() => _FlutterAnimationWidgetState();
}

class _FlutterAnimationWidgetState extends State<FlutterAnimationWidget> with TickerProviderStateMixin {
// ①声明位移动画控制器和颜色动画控制器以及位移动画和颜色动画
AnimationController _animationController;
Animation<double> _animation;
AnimationController _colorAnimationController;
Animation<Color> _colorAnimation;

@override
void initState() {
super.initState();

// ②分别创建位移、颜色的动画控制器和动画
_animationController = AnimationController(duration: Duration(milliseconds: 300), vsync: this);
// Tween对象调用`animate()`函数直接传入上面的AnimationController
_animation = Tween<double>(begin: 0, end: 50).animate(_animationController)
..addListener(() {
setState(() {});
});
_colorAnimationController = AnimationController(duration: Duration(milliseconds: 600), vsync: this);
_colorAnimation = ColorTween(begin: Colors.orangeAccent, end: Colors.redAccent).animate(_colorAnimationController)
..addListener(() {
setState(() {});
});

// ③最后,我们只需要监听位移动画完成状态之后执行颜色动画即可,
_animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_colorAnimationController.forward();
};
});
}

void startEasyAnimation() {
_animationController.forward();
}

@override
void dispose() {
_animationController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 200,
height: 50,
color: _colorAnimation.value,
margin: EdgeInsets.only(top: _animation.value),
),
FlatButton(
onPressed: startEasyAnimation,
child: Text(
"点击执行最简单动画",
style: TextStyle(color: Colors.black38),
),
),
],
),
),
);
}
}

2、Interval时间间隔法

上面的状态监听需要一个动画过程就写一个Controller,而且基本上还要每一个Controller都监听执行完成然后再去启动下一个Controller.如果一个动画过程有十几个,自己想想都是脑瓜子嗡嗡的.所以接下来我们就来介绍第二种方案 - Interval时间间隔法 .

Interval时间间隔法 的整体思路是一个动画Controller控制所有动画的执行.然后每一个动画只需要确认自己在整个动画的时间比重即可.

整体Demo代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
class FlutterAnimationWidget extends StatefulWidget {
TurnTableLotteryPage({Key key}) : super(key: key);

@override
_FlutterAnimationWidgetState createState() => _FlutterAnimationWidgetState();
}

class _FlutterAnimationWidgetState extends State<FlutterAnimationWidget> with TickerProviderStateMixin {
// ①声明一个动画Controller和多个动画.
AnimationController _animationController;
Animation<double> _animation;
Animation<Color> _colorAnimation;

@override
void initState() {
super.initState();

// 然初始化AnimationController,AnimationController的动画时间(**duration**)要设置成所有动画的总时长,例如这里我设定为600毫秒(_animation时长:300毫秒,_colorAnimation时长:300毫秒).
_animationController = AnimationController(duration: Duration(milliseconds: 600), vsync: this);
// Tween对象调用`animate()`函数不再是直接传入上面的AnimationController,而是传入一个 CurvedAnimation 对象。
// 其中CurvedAnimation构建过程中需要传入两个参数一个是 parent ,用于指定AnimationController. 另外一个是 curve,用于指定动画曲线函数.我们可以使用常用的动画曲线函数,也可以自己生成,这里我们就自己生成.指定动画执行的时间区间.
_animation = Tween<double>(begin: 0, end: 50).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(0.0, 0.5), // 由于两个动画时间长度是对分的,每一个都是300毫秒,所以 curve 参数中的值就分别是 **Interval(0.0, 0.5)**、**Interval(0.5, 1.0)**
),
)..addListener(() {
setState(() {});
});
_colorAnimation = ColorTween(begin: Colors.orangeAccent, end: Colors.redAccent).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(0.5, 1.0),// 由于两个动画时间长度是对分的,每一个都是300毫秒,所以 curve 参数中的值就分别是 **Interval(0.0, 0.5)**、**Interval(0.5, 1.0)**
),
)..addListener(() {
setState(() {});
});
}

void startEasyAnimation() {
_animationController.forward();
}

@override
void dispose() {
_animationController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 200,
height: 50,
color: _colorAnimation.value,
margin: EdgeInsets.only(top: _animation.value),
),
FlatButton(
onPressed: startEasyAnimation,
child: Text(
"点击执行最简单动画",
style: TextStyle(color: Colors.black38),
),
),
],
),
),
);
}
}

3、TweenSequence动画序列法

上面的两种方案虽然能解决动画组的问题,但是都太过于繁琐,那么有没有一种比较优雅的方案呢?这就需要使用到 TweenSequenceTweenSequenceItem 这两个类了. 其中 TweenSequence 是动画组类,TweenSequenceItem 则是用来定义每一个动画的具体实现的类.但是TweenSequenceTweenSequenceItem也不是尽善尽美的,它最大的问题就是前后变化的属性值类型必须是一致的.

下面,我们仍然以改变Margin为例, 先让视图组件往上移动50,再让视图 来看看如何使用 TweenSequenceTweenSequenceItem 来实现这个动画.

整体代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class FlutterAnimationWidget extends StatefulWidget {
TurnTableLotteryPage({Key key}) : super(key: key);

@override
_FlutterAnimationWidgetState createState() => _FlutterAnimationWidgetState();
}

class _FlutterAnimationWidgetState extends State<FlutterAnimationWidget> with TickerProviderStateMixin {
// ①声明一个 动画控制器AnimationController 和 动画Animation.
AnimationController _animationController;
Animation<double> _animation;

@override
void initState() {
super.initState();
// 仍然以两者的动画总时长为600毫秒为例
_animationController = AnimationController(duration: Duration(milliseconds: 600), vsync: this);

// 然后,我们通过 **TweenSequence** 和 **TweenSequenceItem** 这两个类对 动画Animation 进行实现.
// 实现两个 **TweenSequenceItem**, **TweenSequenceItem**中的 <font color=red>weight</font> 属性是来设定动画执行的时间权重.也就是在整个动画过程,当前动画执行时长占总时长的比例.例如下面 第一个动画插值占的时间比例为 `50/(50 + 100)`. 第二个动画插值占的时间比例为 `100/(50 + 100)` .
TweenSequenceItem downMarginItem = TweenSequenceItem<double>(
tween: Tween(begin: 1.0, end: 50.0),
weight: 50,
);
TweenSequenceItem upMarginItem = TweenSequenceItem<double>(
tween: Tween(begin: 50.0, end: 100.0),
weight: 100,
);
// 然后创建一个动画插值组,把上面两个动画插值放入组中.
TweenSequence tweenSequence = TweenSequence<double>([
downMarginItem,
upMarginItem,
]);
// 最后,生成动画就OK了.
_animation = tweenSequence.animate(_animationController);
_animation.addListener(() {
setState(() {});
});
}

void startEasyAnimation() {
_animationController.forward();
}

@override
void dispose() {
_animationController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 200,
height: 50,
color: Colors.orangeAccent,
margin: EdgeInsets.only(top: _animation.value),
),
FlatButton(
onPressed: startEasyAnimation,
child: Text(
"点击执行最简单动画",
style: TextStyle(color: Colors.black38),
),
),
],
),
),
);
}
}
动画组实现总结

上面三种实现动画组基本上已经说完了,接下来我们就来对比其不同点.

特性 监听状态法 Interval时间间隔法 TweenSequence动画序列法
代码简洁度 🔅🔅 🔅🔅🔅 🔅🔅🔅🔅🔅
动画是否可交织
动画属性是否可以多变

动画是否可交织 : 动画可否交织主要是说两个动画之间是否需要上一个动画完全执行完成之后,下一个动画才能执行.

动画属性是否可以多变 : 动画属性多变是指当前动画过程中可变化的属性是否可以有多个,例如同时变化尺寸和颜色等等.

第7节:详解Animation-1基础

[TOC]

详解Animation-1基础

参考文章:

一、动画入门

请点击进入阅读:【Flutter】Animation 动画 ( Flutter 动画基本流程 | 创建动画控制器 | 创建动画 | 设置值监听器 | 设置状态监听器 | 布局中使用动画值 | 动画运行 )

二、Animation、AnimationController、Tween

以下内容摘自:Flutter动画 2 - Animation、Tween 详解

Tween的唯一职责就是定义从输入范围到输出范围的映射。输入范围通常为0.0到1.0

1
final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0);

AnimationController 首先是继承于 Animation 的, 相对Animation而言,AnimationController作用则是控制动画.它可以设置动画的时长,并且包含动画的执行方法,具体如下表格所示.

AnimationController 的常用操作说明:

动画执行方法 说明
forward() 正向开始执行动画
reverse() 反向开始执行动画
reset() 重置动画到初始状态
dispose() 取消/停止动画

flutter AnimationStatus 动画状态说明:

AnimationStatus 动画状态值 说明
AnimationStatus.forward 执行 controller.forward() 会回调此状态
AnimationStatus.reverse 执行 controller.reverse() 会回调此状态
AnimationStatus.dismissed 动画从 controller.reverse() 反向执行 结束时会回调此方法
AnimationStatus.completed 动画从 controller.forward() 正向执行 结束时会回调此方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
_animationController = AnimationController(duration: Duration(milliseconds: 300), 

// 位移动画
_positionAnimation = Tween<double>(begin: 0, end: 50).animate(_animationController)
..addListener(() {
setState(() {});
});

// 颜色动画
_colorAnimation = ColorTween(begin: Colors.orangeAccent, end: Colors.redAccent).animate(_animationController)
..addListener(() {
setState(() {});
});

_animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
print("动画完成");
}
});

三、旋转动画 RotationTransition

End