From 5dcff6dcddb08392d0b317f2a07258962b5908e3 Mon Sep 17 00:00:00 2001 From: yuanyj Date: Fri, 22 Sep 2023 12:10:25 +0800 Subject: [PATCH] Site updated: 2023-09-22 12:10:25 --- .../09/13/Flutter/Flutter_state_01/index.html | 45 +- 2023/09/13/hello-world/index.html | 23 +- .../Dart\345\255\246\344\271\240/index.html" | 916 ++++++++++++++ .../Dart\345\260\217\350\256\260/index.html" | 958 +++++++++++++++ .../index.html" | 903 ++++++++++++++ .../index.html" | 940 ++++++++++++++ .../index.html" | 1075 +++++++++++++++++ .../index.html" | 894 ++++++++++++++ 404.html | 6 +- about/index.html | 6 +- archives/2023/09/index.html | 44 +- archives/2023/index.html | 44 +- archives/index.html | 44 +- categories/Dart/index.html | 685 +++++++++++ categories/Flutter/index.html | 26 +- categories/index.html | 8 +- index.html | 398 +++++- search.xml | 338 +++++- tags/Dart/index.html | 691 +++++++++++ tags/Flutter/index.html | 32 +- tags/in-app-purchase/index.html | 685 +++++++++++ tags/index.html | 8 +- "tags/\345\260\217\350\256\260/index.html" | 685 +++++++++++ .../index.html" | 6 +- 24 files changed, 9391 insertions(+), 69 deletions(-) create mode 100644 "2023/09/22/Dart\345\255\246\344\271\240/index.html" create mode 100644 "2023/09/22/Dart\345\260\217\350\256\260/index.html" create mode 100644 "2023/09/22/Flutter/Flutter-\345\206\205\350\264\255\345\256\236\347\216\260/index.html" create mode 100644 "2023/09/22/Flutter/Flutter-\350\277\233\351\230\266/index.html" create mode 100644 "2023/09/22/Flutter/Flutter\345\270\270\347\224\250\347\273\204\344\273\266/index.html" create mode 100644 "2023/09/22/Flutter/\347\250\200\346\234\211\347\273\204\344\273\266/index.html" create mode 100644 categories/Dart/index.html create mode 100644 tags/Dart/index.html create mode 100644 tags/in-app-purchase/index.html create mode 100644 "tags/\345\260\217\350\256\260/index.html" diff --git a/2023/09/13/Flutter/Flutter_state_01/index.html b/2023/09/13/Flutter/Flutter_state_01/index.html index 7327b6c..dd0a783 100644 --- a/2023/09/13/Flutter/Flutter_state_01/index.html +++ b/2023/09/13/Flutter/Flutter_state_01/index.html @@ -18,15 +18,15 @@ - + - + - + @@ -76,7 +76,7 @@ @@ -230,7 +230,7 @@
  • - PORTFOLIO + 复盘-1
  • @@ -345,7 +345,7 @@
  • - PORTFOLIO + 复盘-1
  • @@ -388,7 +388,7 @@

    Flutter State-01

    Jared Yuan - Lv1 + Lv2
    @@ -402,8 +402,8 @@

    Flutter State-01

    @@ -461,7 +461,13 @@

    Flutter State-01

    - + + +

    自定义 Provider

    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
    class CustomProvider<T extends Listenable> extends StatefulWidget {
    final T Function() create;
    final Widget child;
    const CustomProvider({Key? key, required this.child, required this.create}) : super(key: key);

    @override
    State<CustomProvider> createState() => _CustomState<T>();

    static T of<T>(BuildContext context,{bool listen = true}) {
    if(listen) {
    return context.dependOnInheritedWidgetOfExactType<CustomInheritedWidget<T>>()!.model;
    } else {
    return (context.getElementForInheritedWidgetOfExactType<CustomInheritedWidget<T>>()!.widget as CustomInheritedWidget).model;
    }

    }
    }

    class _CustomState<T extends Listenable> extends State<CustomProvider<T>> {

    late T model;

    @override
    void initState() {
    // TODO: implement initState
    super.initState();
    model = widget.create();
    }

    @override
    Widget build(BuildContext context) {
    return AnimatedBuilder(
    animation: model,
    builder: (BuildContext context, Widget? child) {
    return CustomInheritedWidget(model: model,
    child: widget.child);
    },
    );
    }
    }


    class CustomInheritedWidget<T> extends InheritedWidget {
    final T model;

    const CustomInheritedWidget({super.key, required this.model,required super.child});

    static CustomInheritedWidget of<T>(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<CustomInheritedWidget<T>>()!;
    }

    @override
    bool updateShouldNotify(covariant InheritedWidget oldWidget) {
    print("update");
    return true;
    }
    }

    /// extension如果没有取名字只能在本文件中使用,属于私有扩展。起名字可以供其他外部使用
    extension Consumer on BuildContext {
    T watch<T>() => CustomProvider.of(this);
    T
    +
    @@ -475,7 +481,7 @@

    Flutter State-01

  • Updated at - : 2023-09-13 22:06:45 + : 2023-09-22 11:55:20
  • @@ -515,6 +521,21 @@

    Flutter State-01

    +
    + +
    +
    diff --git a/2023/09/13/hello-world/index.html b/2023/09/13/hello-world/index.html index 36a95d5..c950004 100644 --- a/2023/09/13/hello-world/index.html +++ b/2023/09/13/hello-world/index.html @@ -18,15 +18,15 @@ - + - + - + @@ -75,7 +75,7 @@ @@ -229,7 +229,7 @@
  • - PORTFOLIO + 复盘-1
  • @@ -344,7 +344,7 @@
  • - PORTFOLIO + 复盘-1
  • @@ -387,7 +387,7 @@

    Hello World

    Jared Yuan - Lv1 + Lv2
    @@ -401,8 +401,8 @@

    Hello World

    @@ -430,6 +430,7 @@

    Hello World

    Welcome to Hexo ! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub .

    +

    Welcome to!

    This is my blog, sharing somthing interesting with you.

    Quick Start

    Create a new post

    1
    $ hexo new "My New Post"

    More info: Writing

    @@ -456,7 +457,7 @@

    On this page
    Hello World
    -
    +

    diff --git "a/2023/09/22/Dart\345\255\246\344\271\240/index.html" "b/2023/09/22/Dart\345\255\246\344\271\240/index.html" new file mode 100644 index 0000000..e142d33 --- /dev/null +++ "b/2023/09/22/Dart\345\255\246\344\271\240/index.html" @@ -0,0 +1,916 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dart学习 - + + Jshao's Motel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + +
    + + +
    + + + +
    + + +
    + + + +
    + +
    + + + +
    + + +
    +
    + +
    + +

    Dart学习

    + +
    + + + + +
    +
    + +
    +
    +
    + Jared Yuan + + Lv2 + +
    +
    + + +
    +
    +
    + + + + + +
    +

    (一)Duration 是有正负之分

    1、获取常用的参数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import 'Test/Test.dart';

    //Duration类型的使用demo
    main() {
    //定义一个Duration
    Duration stim = Duration(
    days: 0, hours: 1, minutes: 0,
    seconds: 30, milliseconds: 000, microseconds: 00
    );
    //常用操作
    print("获取天数值:${stim.inDays}"); //获取days的值:0
    print("获取总的秒数值:${stim.inSeconds}"); //返回总的秒数,整个时间转换为秒数
    print("获取小时数值:${stim.inMinutes}"); //只会返回小时的值,小时后面的分钟会被忽略
    print(stim.toString()); //转换为字符串
    print(stim.isNegative); //返回此“Duration”是否为负
    print(stim.abs()); //获取Duration绝对值:abs()
    }
    + +

    2、比较两个 Duration 类型的大小,可以应用于比较两个视频的时间长短等等。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import 'Test/Test.dart';

    main() {
    //定义一个Duration
    Duration stim = Duration(
    days: 0, hours: 1, minutes: 0,
    seconds: 30, milliseconds: 000, microseconds: 00
    );
    //定义一个Duration
    Duration stim1=Duration(
    days: 0,hours: 0,minutes: 0, seconds:30,milliseconds: 000,microseconds: 00
    );
    //将stim与stim1比较,等于则返回0,大于返回1,小于返回-1
    print(stim.compareTo(stim1)); //结果:1
    }
    + +

    3、Duration 类型的常用比较,如两个 Duration 相加,相减等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import 'Test/Test.dart';

    //Duration类型的使用demo
    main() {
    //定义一个Duration
    Duration stim = Duration(
    days: 0, hours: 0, minutes: 0,
    seconds: 30, milliseconds: 000, microseconds: 00
    );
    //定义一个Duration
    Duration stim1=Duration(
    days: 0,hours: 0,minutes: 0, seconds:30,milliseconds: 000,microseconds: 00
    );

    print(stim+stim1); //两个Duration相加,结果:0:01:00.000000
    print(stim-stim1); //两个Duration相减,结果:0:00:00.000000
    print(stim*2); //乘 ,结果:0:01:00.000000
    print(stim ~/100); //除,注意有个波浪号~,结果:0:00:00.003000
    }
    + +

    4、Duration 类型截取指定部份显示。

    通过上面的示例不难看出一个问题就是 Duration 显示的时间跨度是带有小数点的,如(0:01:00.000000)。而且显示的是小数点后面 6 位,在实际的应用中,我们只需要显示时:分: 秒的格式(如:0:01:30)。或者像视频进度条里显示的格式(01:30)。那么如何实现这种效果呢?实现方式如下

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     获取Duration-->转为字符串-->利用字符串的截取方法截取指定的位置字符串。
    //Duration类型的使用demo
    main() {
    //定义一个Duration
    Duration stim = Duration(
    days: 0, hours: 0, minutes: 60, seconds: 30, milliseconds: 40, microseconds: 50
    );
    print(stim); //结果:1:00:30.040050
    String str=stim.toString(); //原始数据转为字符串
    print(str.substring(2,7)); //截取指定位置的数据,结果:00:30
    print(str.substring(0,7)); //截取指定位置的数据,结果:1:00:30
    }
    + +

    (二)Timer 定时器

    1
    import 'dart:async';
    + +
    1
    2
    3
    4
    Timer(
    Duration duration,
    void callback()
    )
    + +

    构造方法:

    1
    2
    3
    4
    5
    6
    const timeout = const Duration(seconds: 5);
    print('currentTime ='+DateTime.now().toString());
    Timer(timeout, () {
    //5秒后执行
    print('after 5s Time ='+DateTime.now().toString());
    });
    + +

    Timer.periodic执行多次回调:
    回调多次的定时器用法和回调一次的差不多,区别有下面两点:
    1、API 调用不同
    2、需要手动取消,否则会一直回调,因为是周期性的

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    z`t count = 0;
    const period = const Duration(seconds: 1);
    print('currentTime='+DateTime.now().toString());
    Timer.periodic(period, (timer) {
    //到时回调
    print('afterTimer='+DateTime.now().toString());
    count++;
    if (count >= 5) {
    //取消定时器,避免无限回调
    timer.cancel();
    timer = null;
    }
    });

    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import 'dart:async';

    main() {
    int count = 0;
    Timer timer;
    Timer.periodic(Duration(seconds: 1), (timer) {
    if (count == 5) {
    timer.cancel();
    return;
    }
    count++;
    print(count);
    });
    }

    + +

    (三)时间转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    ///时间转换:
    void transformTime(int times) {
    int date = 0, hour = 0, minute = 0, second = 0;
    if (times > 0) {
    date = times ~/ 86400;
    hour = ((times - 86400 * date) ~/ 3600);
    minute = (times - 86400 * date - hour * 3600) ~/ 60;
    second =
    ((times - 86400 * date - hour * 3600 - minute * 60) % 60).toInt();
    }
    if (times < 60) {
    timer = times < 10 ? ('00:' + '0${second}') : ('00:' + '${second}');
    } else if (times > 3600) {
    timer = '${hour}:' + '${minute}:' + '${second}';
    }
    }
    + +

    (四)字符串

    Dart 的 String 字符串的常用方法

    + +
    + + +
    +
    +
      +
    • Title: Dart学习
    • +
    • Author: Jared Yuan
    • +
    • Created at + : 2023-09-22 12:02:06
    • + +
    • + Updated at + : 2023-09-22 12:09:23 +
    • + +
    • + Link: https://redefine.ohevan.com/2023/09/22/Dart学习/ +
    • +
    • + + License: + + + This work is licensed under CC BY-NC-SA 4.0. + + +
    • +
    +
    + +
    + + + + + + + + + +
    + +
    + +
    + + +
    + +
    + +
    + + + + +
    +
    +
    +
    +  Comments +
    + + + + + +
    + + + + + + +
    + +
    + +
    + + + + +
    + + + + + +
    + + + +
    + + +
    + + +
    +
    +
      + + +
    • + +
    • + + + + +
    • + +
    • + +
    +
    + +
    + + +
    +
    +
      +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + + + + + + +
    • + +
    • +
    + +
      +
    • + +
    • + +
    • + + +
    • + + +
    +
    + +
    + +
    + +
    + + + +
    + +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + +
    + + + + diff --git "a/2023/09/22/Dart\345\260\217\350\256\260/index.html" "b/2023/09/22/Dart\345\260\217\350\256\260/index.html" new file mode 100644 index 0000000..1c33f6e --- /dev/null +++ "b/2023/09/22/Dart\345\260\217\350\256\260/index.html" @@ -0,0 +1,958 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dart小记 - + + Jshao's Motel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + +
    + + +
    + + + +
    + + +
    + + + +
    + +
    + + + +
    + + +
    +
    + +
    + +

    Dart小记

    + +
    + + + + +
    +
    + +
    +
    +
    + Jared Yuan + + Lv2 + +
    +
    + + +
    +
    +
    + + + + + +
    +

    泛型方法:

      +
    • dart 的泛型方法书写和 Java 的不太一样。
    • +
    +
      +
    1. Dart 的泛型方法
    2. +
    +
    1
    2
    3
    4
    5
    6
    T first<T>(List<T> ts) {
    // 进行一些初始工作或错误检查,然后...
    T tmp = ts[0];
    // 进行一些额外的检查或处理...
    return tmp;
    }
    + +

    Tips

    +

    表示这是一个泛型方法,T 是类型参数的名称,

    +
    + +
      +
    1. Java 的泛型方法
    2. +
    +
    1
    2
    3
    4
    5
    6
    7
    <T> T first(List<T> ts) {
    // 进行一些初始工作或错误检查,然后...
    T tmp = ts.get(0);
    // 进行一些额外的检查或处理...
    return tmp;
    }
    <T> 位于方法返回类型之前,表示这是一个泛型方法,T 是类型参数的名称,可以替换成其他合法的标识符。
    + +

    Mixin(混入)

    Tips

    +

    作用:解决无需多重继承即可拥有功能方法
    混合使用 with 关键字,with 后面可以是 class、abstract class 和 mixin 的类型

    +
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // mixin 声明的类
    mixin a {

    }

    // 普通的类
    class b {

    }

    class c with a, b {}
    + +
      +
    • 混入(with)多类,遇到同名方法的情况,按照混入的顺序,后面的会覆盖前面
    • +
    • mixin 的类无法定义构造函数,所以一般会将需要 mixin 的类使用 mixin 关键字
    • +
    • 使用关键字 on 限定混入条件
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Person {
    eat(){
    print("eat");
    }
    }

    class Dog {

    }

    /// 使用关键字 on 限定 Code 只能被 Person 或者其子类 mixin
    /// 添加限定后,可以重写其方法, Code 重写 Person 的方法
    /// super 表示调用父类(Person)的方法。
    mixin Code on Person {
    @override
    eat(){
    super.eat();
    }
    }
    + +
      +
    • 混合后的类型是超类的子类型(类似多继承)
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class F {

    }

    class G {

    }

    class H {

    }

    class FG extends H with F,G {}

    FG fg = FG();
    /_
    fg is F: true
    fg is G: true
    fg is H: true
    _/

    + +

      +
    • 调用非默认超类构造函数
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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().
    Employee.fromJson(super.data) : super.fromJson() {
    print('in Employee');
    }
    }

    void main() {
    var employee = Employee.fromJson({});
    print(employee);
    // Prints:
    // in Person
    // in Employee
    // Instance of 'Employee'
    }
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Vector2d {
    final double x;
    final double y;

    Vector2d(this.x, this.y);
    }

    class Vector3d extends Vector2d {
    final double z;

    // Forward the x and y parameters to the default super constructor like:
    // Vector3d(final double x, final double y, this.z) : super(x, y);
    Vector3d(super.x, super.y, this.z);
    }

    + +
    + + +
    +
    +
      +
    • Title: Dart小记
    • +
    • Author: Jared Yuan
    • +
    • Created at + : 2023-09-22 11:41:25
    • + +
    • + Updated at + : 2023-09-22 12:07:32 +
    • + +
    • + Link: https://redefine.ohevan.com/2023/09/22/Dart小记/ +
    • +
    • + + License: + + + This work is licensed under CC BY-NC-SA 4.0. + + +
    • +
    +
    + +
    + + + + + + + + + +
    + +
    + +
    + + +
    + +
    + +
    + + + + +
    +
    +
    +
    +  Comments +
    + + + + + +
    + + + + + + +
    + +
    + +
    + + +
    +
    +
    +
    On this page
    +
    Dart小记
    + + +
    +
    +
    + +
    + + + + + +
    + + + +
    + + +
    + + +
    +
    +
      + + +
    • + +
    • + + + + +
    • + +
    • + +
    +
    + +
    + + +
    +
    +
      +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + + + + + + +
    • + +
    • +
    + +
      +
    • + +
    • + +
    • + + +
    • + + +
    +
    + +
    + +
    + +
    + + + +
    + +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + +
    + + + + diff --git "a/2023/09/22/Flutter/Flutter-\345\206\205\350\264\255\345\256\236\347\216\260/index.html" "b/2023/09/22/Flutter/Flutter-\345\206\205\350\264\255\345\256\236\347\216\260/index.html" new file mode 100644 index 0000000..aaf05b9 --- /dev/null +++ "b/2023/09/22/Flutter/Flutter-\345\206\205\350\264\255\345\256\236\347\216\260/index.html" @@ -0,0 +1,903 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Flutter 内购实现 - + + Jshao's Motel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + +
    + + +
    + + + +
    + + +
    + + + +
    + +
    + + + +
    + + +
    +
    + +
    + +

    Flutter 内购实现

    + +
    + + + + +
    +
    + +
    +
    +
    + Jared Yuan + + Lv2 + +
    +
    + + +
    +
    +
    + + + + + +
    +
    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
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    import 'dart:async';
    import 'dart:io';
    import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
    import 'package:in_app_purchase_storekit/store_kit_wrappers.dart';
    import 'package:flutter/cupertino.dart';
    import 'package:in_app_purchase/in_app_purchase.dart';

    /// 适用于ios端的内购处理方法
    class InAppPurchaseUtils {
    late final InAppPurchase _inAppPurchase;
    late StreamSubscription<List<PurchaseDetails>> _subscription;
    late final Stream<List<PurchaseDetails>> purchaseUpdated;
    /// 产品id
    static const List<String> productIds = [];
    List<ProductDetails> _products = <ProductDetails>[];

    /// 初始化
    InAppPurchaseUtils.init() {
    _inAppPurchase = InAppPurchase.instance;
    purchaseUpdated = _inAppPurchase.purchaseStream;
    initStoreInfo();
    }

    /// 查询产品信息
    /// 返回的数据类型是ProductDetailsResponse
    /// 获取详细的产品数据:productDetailResponse.productDetails
    /// Example:
    /// if (productDetailResponse.error != null) {
    // setState(() {
    // _queryProductError = productDetailResponse.error!.message;
    // _isAvailable = isAvailable;
    // _products = productDetailResponse.productDetails;
    // _purchases = <PurchaseDetails>[];
    // _notFoundIds = productDetailResponse.notFoundIDs;
    // _consumables = <String>[];
    // _purchasePending = false;
    // _loading = false;
    // });
    // return;
    // }
    Future<ProductDetailsResponse> queryProductDetails() async{
    return await _inAppPurchase.queryProductDetails(productIds.toSet());
    }

    List<ProductDetails> get products => _products;

    /// 初始化商品(ios端,未集成Android)
    Future<void> initStoreInfo() async{
    if(await isAvailable) {
    if (Platform.isIOS) {
    final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition =
    _inAppPurchase
    .getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
    await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());
    }
    queryProductDetails().then((value) {
    if(value.error != null) {
    _products = value.productDetails;
    }
    });
    }
    }

    /// 内购监听
    void addPurchaseListener({required PurchaseListener listener, ValueChanged? onError}) {
    _subscription = purchaseUpdated.listen((List<PurchaseDetails> purchaseDetailsList) {
    _listenToPurchaseUpdated(purchaseDetailsList, listener: listener);
    },onDone: (){
    _subscription.cancel();
    },onError: (Object err){
    if(onError != null) onError(err);
    });
    }

    /// 购买消耗产品(金币)
    void buyConsumable(ProductDetails productDetails){
    if(Platform.isIOS) {
    _inAppPurchase.buyConsumable(purchaseParam: PurchaseParam(productDetails: productDetails));
    }
    }

    /// 订阅
    void buyNonConsumable(ProductDetails productDetails){
    if(Platform.isIOS) {
    _inAppPurchase.buyNonConsumable(purchaseParam: PurchaseParam(productDetails: productDetails));
    }
    }

    /// 处理内购
    Future<void> _listenToPurchaseUpdated(
    List<PurchaseDetails> purchaseDetailsList, {required PurchaseListener listener}) async {
    for (final PurchaseDetails purchaseDetails in purchaseDetailsList) {
    if (purchaseDetails.status == PurchaseStatus.pending) {
    if(listener.onPending != null) listener.onPending!();
    // showPendingUI();
    } else {
    if(listener.onPendingComplete != null) listener.onPendingComplete!();
    if (purchaseDetails.status == PurchaseStatus.error) {
    if(listener.onError != null) listener.onError!(purchaseDetails.error);
    } else if (purchaseDetails.status == PurchaseStatus.purchased ||
    purchaseDetails.status == PurchaseStatus.restored) {
    listener.onPurchased(purchaseDetails);
    // final bool valid = await _verifyPurchase(purchaseDetails);
    // if (valid) {
    // deliverProduct(purchaseDetails);
    // } else {
    // _handleInvalidPurchase(purchaseDetails);
    // return;
    // }
    }
    // if (Platform.isAndroid) {
    // if (!_kAutoConsume && purchaseDetails.productID == _kConsumableId) {
    // final InAppPurchaseAndroidPlatformAddition androidAddition =
    // _inAppPurchase.getPlatformAddition<
    // InAppPurchaseAndroidPlatformAddition>();
    // await androidAddition.consumePurchase(purchaseDetails);
    // }
    // }
    if (purchaseDetails.pendingCompletePurchase) {
    await _inAppPurchase.completePurchase(purchaseDetails);
    }
    }
    }
    }

    /// 内购服务是否可用
    Future<bool> get isAvailable async => await _inAppPurchase.isAvailable();

    /// 取消内购服务监听
    /// dispose 时可调用
    void cancel(){
    if (Platform.isIOS) {
    final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition =
    _inAppPurchase
    .getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
    iosPlatformAddition.setDelegate(null);
    }
    _subscription.cancel();
    }
    }

    class PurchaseListener {
    /// 等待
    VoidCallback? onPending;
    ValueChanged? onError;
    /// 购买事件
    late ValueChanged<PurchaseDetails> onPurchased;
    ///等待结束
    VoidCallback? onPendingComplete;
    PurchaseListener({required this.onPurchased,this.onPending,this.onError,this.onPendingComplete});
    }

    class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper {
    @override
    bool shouldContinueTransaction(
    SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) {
    return true;
    }

    @override
    bool shouldShowPriceConsent() {
    return false;
    }
    }
    + +
    + + +
    +
    +
      +
    • Title: Flutter 内购实现
    • +
    • Author: Jared Yuan
    • +
    • Created at + : 2023-09-22 12:04:46
    • + +
    • + Updated at + : 2023-09-22 12:06:20 +
    • + +
    • + Link: https://redefine.ohevan.com/2023/09/22/Flutter/Flutter-内购实现/ +
    • +
    • + + License: + + + This work is licensed under CC BY-NC-SA 4.0. + + +
    • +
    +
    + +
    + + + + + + + + + +
    + + +
    + +
    + +
    + + + + +
    +
    +
    +
    +  Comments +
    + + + + + +
    + + + + + + +
    + +
    + +
    + + +
    +
    +
    +
    On this page
    +
    Flutter 内购实现
    + + +
    +
    +
    + +
    + + + + + +
    + + + +
    + + +
    + + +
    +
    +
      + + +
    • + +
    • + + + + +
    • + +
    • + +
    +
    + +
    + + +
    +
    +
      +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + + + + + + +
    • + +
    • +
    + +
      +
    • + +
    • + +
    • + + +
    • + + +
    +
    + +
    + +
    + +
    + + + +
    + +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + +
    + + + + diff --git "a/2023/09/22/Flutter/Flutter-\350\277\233\351\230\266/index.html" "b/2023/09/22/Flutter/Flutter-\350\277\233\351\230\266/index.html" new file mode 100644 index 0000000..daecd0e --- /dev/null +++ "b/2023/09/22/Flutter/Flutter-\350\277\233\351\230\266/index.html" @@ -0,0 +1,940 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Flutter 进阶 - + + Jshao's Motel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + +
    + + +
    + + + +
    + + +
    + + + +
    + +
    + + + +
    + + +
    +
    + +
    + +

    Flutter 进阶

    + +
    + + + + +
    +
    + +
    +
    +
    + Jared Yuan + + Lv2 + +
    +
    + + +
    +
    +
    + + + + + +
    +

    1.provider

    https://blog.csdn.net/lpfasd123/article/details/101573980

    +

    2.InheritedWidget

    1

    + +

    3.provider

    Provider,Google 官方推荐的一种 Flutter 页面状态管理组件,它的实质其实就是对 InheritedWidget 的包装,使它们更易于使用和重用。

    +
      +
    • 创建一个 ChangeNotifier

      +
      1
      2
      3
      4
      5
      6
      7
      8
      class Model1 extends ChangeNotifier {
      int _count = 1;
      int get value => _count;
      set value(int value) {
      _count = value;
      notifyListeners();
      }
      }
      +
    • +
    • 创建一个 ChangeNotifier(方式一)

      +

      这里通过 ChangeNotifierProvider 的 create 把 ChangeNotifier(即 Model1)建立联系,作用域的范围在 child 指定的 MaterialApp,这里我们将 SingleStatsView 作为首页,SingleStatsView 里面使用了 Model1 作为数据源。需要注意的是,不要把所有状态的作用域都放在 MaterialApp,根据实际业务需求严格控制作用域范围,全局状态多了会严重影响应用的性能。

      +
      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
      return ChangeNotifierProvider(
      create: (context) {
      return Model1();
      },
      child: MaterialApp(
      theme: ArchSampleTheme.theme,
      home: SingleStatsView(),
      ),
      );

      class SingleStatsView extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
      return Center(
      child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
      FlatButton(
      color: Colors.blue,
      child: Text('Model1 count++'),
      onPressed: () {
      Provider.of<Model1>(context, listen: false).count++;
      },
      ),
      Padding(
      padding: const EdgeInsets.only(bottom: 8.0),
      child: Text('Model count值变化监听',
      style: Theme.of(context).textTheme.title),
      ),
      Padding(
      padding: const EdgeInsets.only(bottom: 8.0),
      child: Text('Model1 count:${Provider.of<Model1>(context).count}',
      style: Theme.of(context)
      .textTheme
      .subhead
      .copyWith(color: Colors.green)),
      ),
      ],
      ),
      );
      }
      }
      +
    • +
    • 创建一个 ChangeNotifierProvider.value(方式二)

      +
      1
      2
      3
      4
      5
      6
      return ChangeNotifierProvider.value(
      value: Model1(),
      child: MaterialApp(
      theme: ArchSampleTheme.theme,
      home: SingleStatsView(),
      ));
      +
    • +
    • 在页面中监听状态变更,其他使用方式

      +

      ValueListenableBuilder

      +
    • +
    +

    Stream 的相关案例

    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
    import 'dart:async';

    class DataBloc {

    factory DataBloc() => _instance;

    static final DataBloc _instance = DataBloc._init();

    ///定义一个Controller
    StreamController<String> _dataController = StreamController.broadcast(
    onListen: (){
    print('databloc listen');
    },
    onCancel: (){
    print('databloc cancel');
    }
    );
    ///获取 StreamSink 做 add 入口
    StreamSink<String> get dataSink => _dataController.sink;
    ///获取 Stream 用于监听
    Stream<String> get dataStream => _dataController.stream;
    ///事件订阅对象
    late StreamSubscription _dataSubscription;

    DataBloc._init() {
    ///监听事件
    _dataSubscription = dataStream.listen((value){
    ///do change
    });

    }

    close() {
    ///关闭
    _dataSubscription.cancel();
    _dataController.close();
    }
    }
    + +
    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
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    import 'dart:async';
    import 'dart:async';
    import 'dart:io';
    import 'package:logger/logger.dart';
    import 'package:amap_flutter_location/amap_flutter_location.dart';
    import 'package:amap_flutter_location/amap_location_option.dart';

    var logger = Logger(
    printer: PrettyPrinter(methodCount: 1, printEmojis: false, colors: false),
    );
    /// 基于定位设计的Bloc
    class LocationBloc {
    /// 实现单例模式
    factory LocationBloc() => _instance;

    static final LocationBloc _instance = LocationBloc._init();

    // static LocationBloc get instance => _instance;

    LocationBloc._init() {
    if (Platform.isIOS) {
    _requestAccuracyAuthorization();
    }
    ///监听事件
    _dataSubscription = _locationPlugin.onLocationChanged().listen((Map<String, Object>? result) {
    _dataController.sink.add(result);
    });
    _startLocation();


    }

    static AMapFlutterLocation _locationPlugin = new AMapFlutterLocation();
    ///定义一个Controller
    StreamController<Map<String, Object>?> _dataController = StreamController.broadcast();
    ///获取 StreamSink 做 add 入口
    // StreamSink<List<String>> get _dataSink => _dataController.sink;
    ///获取 Stream 用于监听
    Stream<Map<String, Object>?> get dataStream => _dataController.stream;
    ///事件订阅对象
    late StreamSubscription _dataSubscription;

    ///设置定位参数
    static void _setLocationOption() {
    if (null != _locationPlugin) {
    AMapLocationOption locationOption = new AMapLocationOption();

    ///是否单次定位
    locationOption.onceLocation = false;

    ///是否需要返回逆地理信息
    locationOption.needAddress = true;

    ///逆地理信息的语言类型
    locationOption.geoLanguage = GeoLanguage.DEFAULT;

    locationOption.desiredLocationAccuracyAuthorizationMode =
    AMapLocationAccuracyAuthorizationMode.ReduceAccuracy;

    locationOption.fullAccuracyPurposeKey = "AMapLocationScene";

    ///设置Android端连续定位的定位间隔(源码里面默认应该是2秒)
    locationOption.locationInterval = 10000;

    ///设置Android端的定位模式<br>
    ///可选值:<br>
    ///<li>[AMapLocationMode.Battery_Saving]</li>
    ///<li>[AMapLocationMode.Device_Sensors]</li>
    ///<li>[AMapLocationMode.Hight_Accuracy]</li>
    locationOption.locationMode = AMapLocationMode.Hight_Accuracy;

    ///设置iOS端的定位最小更新距离<br>
    locationOption.distanceFilter = -1;

    ///设置iOS端期望的定位精度
    /// 可选值:<br>
    /// <li>[DesiredAccuracy.Best] 最高精度</li>
    /// <li>[DesiredAccuracy.BestForNavigation] 适用于导航场景的高精度 </li>
    /// <li>[DesiredAccuracy.NearestTenMeters] 10米 </li>
    /// <li>[DesiredAccuracy.Kilometer] 1000米</li>
    /// <li>[DesiredAccuracy.ThreeKilometers] 3000米</li>
    locationOption.desiredAccuracy = DesiredAccuracy.Best;

    ///设置iOS端是否允许系统暂停定位
    locationOption.pausesLocationUpdatesAutomatically = false;

    ///将定位参数设置给定位插件
    _locationPlugin.setLocationOption(locationOption);
    }
    }

    ///获取iOS native的accuracyAuthorization类型
    static void _requestAccuracyAuthorization() async {
    AMapAccuracyAuthorization currentAccuracyAuthorization =
    await _locationPlugin.getSystemAccuracyAuthorization();
    if (currentAccuracyAuthorization ==
    AMapAccuracyAuthorization.AMapAccuracyAuthorizationFullAccuracy) {
    print("精确定位类型");
    } else if (currentAccuracyAuthorization ==
    AMapAccuracyAuthorization.AMapAccuracyAuthorizationReducedAccuracy) {
    print("模糊定位类型");
    } else {
    print("未知定位类型");
    }
    }

    ///开始定位
    static void _startLocation() {
    if (null != _locationPlugin) {
    ///开始定位之前设置定位参数
    _setLocationOption();
    _locationPlugin.startLocation();
    }
    }

    close() {
    ///关闭
    logger.d('移除定位订阅');
    _dataSubscription.cancel();
    _dataController.close();
    }
    }

    + +

    文章:https://cloud.tencent.com/developer/article/1511980

    +

    https://cloud.tencent.com/developer/article/1610790

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class StateSubject {
    static final StateSubject _instance = StateSubject._();

    factory StateSubject() => StateSubject._instance;

    StreamController<int> streamController;

    StateSubject._() {
    streamController = StreamController.broadcast();
    }

    void update(int num) {
    streamController.sink.add(num);
    }
    }
    + +

    Flutter ios 设置中文语言

    1、先把手机的语言模式设置成简体中文

    +

    2、在 Info.Plist 里面把 Localization native development region 字段修改成 China

    +

    3、在 Info.Plist 里面添加字段 Localized resources can be mixed(Boolean)值为 YES

    +

    方法都设置好了后,打开相机调用的还是英文

    +

    还要在项目的 PROJECT -> Info -> Localizations 中添加语言包才可以
    ————————————————
    版权声明:本文为 CSDN 博主「moon 清泉」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/MoonAndroid/article/details/121625582

    + +
    + + +
    +
    +
      +
    • Title: Flutter 进阶
    • +
    • Author: Jared Yuan
    • +
    • Created at + : 2023-09-22 12:02:59
    • + +
    • + Updated at + : 2023-09-22 12:06:44 +
    • + +
    • + Link: https://redefine.ohevan.com/2023/09/22/Flutter/Flutter-进阶/ +
    • +
    • + + License: + + + This work is licensed under CC BY-NC-SA 4.0. + + +
    • +
    +
    + +
    + + + + + + + + + +
    + +
    + +
    + + +
    + +
    + +
    + + + + +
    +
    +
    +
    +  Comments +
    + + + + + +
    + + + + + + +
    + +
    + +
    + + + + +
    + + + + + +
    + + + +
    + + +
    + + +
    +
    +
      + + +
    • + +
    • + + + + +
    • + +
    • + +
    +
    + +
    + + +
    +
    +
      +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + + + + + + +
    • + +
    • +
    + +
      +
    • + +
    • + +
    • + + +
    • + + +
    +
    + +
    + +
    + +
    + + + +
    + +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + +
    + + + + diff --git "a/2023/09/22/Flutter/Flutter\345\270\270\347\224\250\347\273\204\344\273\266/index.html" "b/2023/09/22/Flutter/Flutter\345\270\270\347\224\250\347\273\204\344\273\266/index.html" new file mode 100644 index 0000000..79433b5 --- /dev/null +++ "b/2023/09/22/Flutter/Flutter\345\270\270\347\224\250\347\273\204\344\273\266/index.html" @@ -0,0 +1,1075 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Flutter常用组件 - + + Jshao's Motel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + +
    + + +
    + + + +
    + + +
    + + + +
    + +
    + + + +
    + + +
    +
    + +
    + +

    Flutter常用组件

    + +
    + + + + +
    +
    + +
    +
    +
    + Jared Yuan + + Lv2 + +
    +
    + + +
    +
    +
    + + + + + +
    +

    1.圆角按钮

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    TextButton(
    onPressed: (){
    print('取消订单');
    },
    style: TextButton.styleFrom(
    padding: EdgeInsets.zero,
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(16.0),
    side: BorderSide(width: 1.0, color: Color(0xFFC4C8CC)) /// 外边框
    ),
    minimumSize: Size.zero
    ),
    child: Container(
    padding: EdgeInsets.symmetric(horizontal: 18.0, vertical: 5.0),
    child: Text('取消订单', style: TextStyle(color: Color(0xFFA8ADB3), fontSize: 12.0)),
    )
    ),
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ClipRRect(
    borderRadius: BorderRadius.circular(16.5),
    child: ElevatedButton(
    onPressed: () {
    Nav.push((context) => CheckoutCounter());
    },
    style: ButtonStyle(
    textStyle: MaterialStateProperty.all(TextStyle(fontSize: 12.0)),
    backgroundColor: MaterialStateProperty.all(Color(0xFF3F5AFF)),
    padding: MaterialStateProperty.all(EdgeInsets.symmetric(vertical: 8.0, horizontal: 26.0))
    ),
    child: Text("提交订单"),
    ),
    )
    + +

    2.Text

    1
    Text('待发货', style: TextStyle(color: Color(0xFF223359), fontSize: 14.0)),
    + +

    3.圆形 Container

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Container(
    height: 200,
    width: 200,
    decoration: BoxDecoration(
    image: DecorationImage(
    image: NetworkImage(
    'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg'),
    fit: BoxFit.cover,
    ),
    border: Border.all(
    color: Colors.blue,
    width: 2,
    ),
    shape: BoxShape.circle, /// 一般这个属性就够了
    ),
    )
    + +

    4. 圆形按钮

    CSDN:关于 Flutter 的 button 的按钮 ElevatedButton

    +
    1
    InviteNumScreen
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    ElevatedButton(
    child: Text('button'),
    onPressed: (){},
    style: ButtonStyle(
    padding: MaterialStateProperty.all(EdgeInsets.all(20.0)),
    textStyle: MaterialStateProperty.all(TextStyle(fontSize: 20)),
    shape: MaterialStateProperty.all(
    CircleBorder(
    side: BorderSide(
    color: Colors.deepOrangeAccent,
    width: 1.0
    )
    )
    )
    )
    )
    + +

    5.自定义对话框

      +
    1. 调用方式

      +
      1
      bool result = await showDialog(context: context, builder: (context) => CustomDialog('确定退出紫鲸书苑?'));
      +
    2. +
    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
      return Dialog(
      child: Container(
      decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(10.0)
      ),
      height: 123.0,
      child: Column(
      children: [
      Container(
      padding: EdgeInsets.symmetric(vertical: 30.0),
      child: Center(
      child: Text(_message, style: TextStyle(color: Color(0xFF223359), fontSize: 16.0)),
      ),
      ),
      Expanded(
      child: Row(
      children: [
      Flexible(
      child: Container(
      decoration: BoxDecoration(
      border: Border(
      top: BorderSide(width: 1.0, color: Color(0xFFEBEDF0)),
      right: BorderSide(width: 1.0, color: Color(0xFFEBEDF0))
      ),
      ),
      child: SizedBox.expand(
      child: TextButton(
      onPressed: (){
      Nav.pop(false);
      },
      child: Text('取消', style: TextStyle(fontSize: 14.0 ,color: Color(0xFFA8ADB3))),
      ),
      ),
      ),
      ),
      Flexible(
      child: Container(
      decoration: BoxDecoration(
      border: Border(
      top: BorderSide(width: 1.0, color: Color(0xFFEBEDF0))
      ),
      ),
      child: SizedBox.expand(
      child: TextButton(
      onPressed: (){
      Nav.pop(true);
      },
      child: Text('确定', style: TextStyle(fontSize: 14.0 ,color: Color(0xFF223359))),
      ),
      ),
      ),
      )
      ],
      )
      )],
      ),
      ),
      );
    4. +
    +

    6.圆角按钮

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    ElevatedButton(
    onPressed: (){
    print('保存');
    },
    style: ButtonStyle(
    shape: MaterialStateProperty.all(
    RoundedRectangleBorder(borderRadius: BorderRadius.circular(25.0))
    ),
    ),
    child: Text('保存')
    )


    ElevatedButton(
    onPressed: (){

    },
    style: ElevatedButton.styleFrom(
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5.0)),
    padding: EdgeInsets.symmetric(horizontal: 27.0, vertical: 7.0),
    ),
    child: Text('确定')
    )
    + +

    7.独占一行 TextFormField

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    TextFormField(
    decoration: InputDecoration(
    contentPadding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 16.0),
    filled: true,
    fillColor: Colors.white,
    hintText: '请输入',
    hintStyle: TextStyle(color: Color(0xFFA8ADB3), fontSize: 14.0),
    helperText: '请不要超过16个字符,支持中英文、数字',
    helperStyle: TextStyle(color: Color(0xFFC4C8CC), fontSize: 13.0),
    border: InputBorder.none
    ),
    style: TextStyle(fontSize: 14.0),
    ),
    + +

    8.独占一行的圆角按钮

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    Padding(
    padding: EdgeInsets.symmetric(horizontal: 40.0),
    child: Row(
    children: [
    Expanded(
    child: ElevatedButton(
    onPressed: (){
    print('保存');
    },
    style: ButtonStyle(
    backgroundColor: MaterialStateProperty.all(Color(0xFF3F5AFF)),
    padding: MaterialStateProperty.all(EdgeInsets.symmetric(vertical: 12.0)),
    shape: MaterialStateProperty.all(
    RoundedRectangleBorder(borderRadius: BorderRadius.circular(25.0))
    ),
    ),
    child: Text('保存', style: TextStyle(fontSize: 14.0))
    ),
    ),
    ],
    ),
    )
    + +

    9.文本输入框不限制行数,类似富文本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    TextFormField(
    maxLines: null,
    expands: true,
    style: TextStyle(fontSize: 13.0),
    decoration: InputDecoration(
    contentPadding: EdgeInsets.zero,
    hintText: '填写详细地址',
    hintStyle: TextStyle(fontSize: 13.0, color: Color(0xFFA8ADB3)),
    border: InputBorder.none
    ),
    )
    + +
    1
    2
    3
    4
    separatorBuilder: (BuildContext context, int index){
    return Divider(height: 2.0, color: Color(0xFFEBEBEB));
    },
    itemCount: _linkList.length,
    + +

    10.有关圆角设置

    1
    2
    3
    4
    5
    6
    7
    decoration: BoxDecoration(
    borderRadius: BorderRadius.only(
    topLeft: Radius.circular(30.0),
    topRight: Radius.circular(30.0)
    ),
    color: Colors.white,
    ),
    + +

    11.有关 showModalBottomSheet

    解决圆角底部弹窗

    +

    在 showModalBottomSheet 里面的根容器设置成 SingleChildScrollView,即可实现高度根据内容自适应

    +
    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
    showModalBottomSheet(/// 底部弹窗
    context: context,
    enableDrag: false,
    backgroundColor: Colors.white,
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.only(
    topLeft: Radius.circular(30.0),
    topRight: Radius.circular(30.0),
    )
    ),
    builder: (BuildContext context) {
    return SingleChildScrollView(
    child: Container(
    padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 15.0),
    child: Column(
    children: [
    Text('The cancellation of account', style: TextStyle(color: Color(0xFF333333), fontSize: 16.0)),
    Padding(
    padding: const EdgeInsets.symmetric(vertical: 15.0),
    child: Text('Please note: Once your account is logged out,'
    ' you will not be able to log in and use your account, '
    'and your rights and interests will be cleared and cannot be restored.'
    ' Are you sure to cancel your account?',
    style: TextStyle(color: Color(0xFF999999), fontSize: 12.0),
    ),
    ),
    Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
    TextButton(
    onPressed: (){
    print('取消订单');
    },
    style: TextButton.styleFrom(
    backgroundColor: Color(0xFF5BCD49),
    padding: EdgeInsets.zero,
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(13.5)/// 外边框
    ),
    minimumSize: Size.zero
    ),
    child: Container(
    padding: EdgeInsets.symmetric(horizontal: 13.0, vertical: 5.0),
    child: Text('confirm', style: TextStyle(color: Colors.white, fontSize: 12.0)),
    )
    ),
    TextButton(
    onPressed: (){
    print('取消订单');
    },
    style: TextButton.styleFrom(
    padding: EdgeInsets.zero,
    backgroundColor: Colors.white,
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(13.5),
    side: BorderSide(width: 0.5, color: Color(0xFF999999)) /// 外边框
    ),
    minimumSize: Size.zero
    ),
    child: Container(
    padding: EdgeInsets.symmetric(horizontal: 13.0, vertical: 5.0),
    child: Text('cancel', style: TextStyle(color: Color(0xFF999999), fontSize: 12.0)),
    )
    ),
    ],
    )
    ],
    ),
    ),
    );
    },
    );
    + +

    12.列表展示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /// 列表展示
    ListView.separated(
    shrinkWrap: true,
    padding: EdgeInsets.zero,
    physics: NeverScrollableScrollPhysics(),
    separatorBuilder: (BuildContext context, int index) {
    return Divider(
    height: 1.0,
    );
    },
    itemBuilder: (context, index) {
    return ShopItem(_list[index]);
    },
    itemCount: _list.length,
    )
    + +

    13.Container 阴影

    1
    2
    3
    4
    5
    6
    7
    8
    boxShadow: [
    BoxShadow(
    color: Colors.black12,
    offset: Offset(0.0, 15.0), //阴影xy轴偏移量
    blurRadius: 15.0, //阴影模糊程度
    spreadRadius: 1.0 //阴影扩散程度
    )
    ],
    + +

    14.TextFormField 属性

    15.Flutter 的 showModalBottomSheet 输入框被弹出的键盘挡住

    16.页面滚动,性能优越的结构

      +
    1. Column -> Expanded -> ListView.builder
    2. +
    +

    17.圆角 TextFormField

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    TextFormField(
    cursorColor: Color(0xFF5BCD49),
    decoration: InputDecoration(
    filled: true,
    fillColor: Colors.white,
    border: OutlineInputBorder(
    borderRadius: BorderRadius.circular(10.0),
    borderSide: BorderSide.none
    ),
    hintText: 'Enter a new binding mailbox',
    hintStyle: TextStyle(
    color: Color(0xFFD6D9DD), fontSize: 14.0)),
    ),
    + +

    18.固定在界面上的时间选择器

    相关链接:

    +

    https://blog.csdn.net/mengks1987/article/details/104580596

    +

    https://segmentfault.com/a/1190000020205762

    +

    19.ListWheelScrollView.useDelegate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    ListWheelScrollView.useDelegate(
    itemExtent: 50.0,
    diameterRatio: .5,
    childDelegate: ListWheelChildBuilderDelegate(
    builder: (context, index) {
    return Container(
    alignment: Alignment.center,
    color: Colors.primaries[index % 10],
    child: Text('$index'),
    );
    }
    ),
    /// 选中事件
    onSelectedItemChanged: (val){
    print(val);
    },
    )
    + +

    20.TextFormField 文章

    1
    http://www.ptbird.cn/flutter-form-textformfield.html
    + +
    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
    Column(
    children: [
    Flexible(
    child: ListView(
    shrinkWrap: true,
    children: _links.map((item) =>
    GestureDetector(
    onTap: (){
    Nav.push((context) => item['link']);
    },
    child: Container(
    margin: EdgeInsets.only(top: 5.0),
    height: 50.0,
    padding: EdgeInsets.only(left: 15.0, right: 17.0),
    color: Colors.white,
    child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
    Text(item['title'], style: TextStyle(color: Color(0xFF333333), fontSize: 14.0)),
    Image.asset('arrow_click'.webp, width: 6.0, height: 12.0, fit: BoxFit.cover)
    ],
    ),
    ),
    )
    ).toList(),
    ),
    ),
    ],
    )

    + +

    21.Entry name ‘classes.dex‘ collided

    有效的解决方案是把 release 文件夹下之前生成的 apk 删除,然后再次生成 apk。

    +

    22.flutter textformfield 失去焦点

    https://www.cnblogs.com/lude1994/p/14218014.html

    +
    1
    Text('Exhaust Fan 1')
    + +

    23.InkWell 使用无效

    在外层嵌套个 inkwell,如果还是不行就在 inkwell 外面套一个 Material

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Material(
    color: Colors.transparent,
    child: InkWell(
    onLongPress : (){
    print('fff');
    },
    child: Container(child: Text('Exhaust Fan 1'))
    ),
    ),
    + +

    24.瀑布流布局

    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
    StaggeredGridView.countBuilder(
    padding: EdgeInsets.symmetric(horizontal: 15.0, vertical: 20.0),
    shrinkWrap: true,
    physics: NeverScrollableScrollPhysics(),
    primary: true,
    //滑动方向
    scrollDirection: Axis.vertical,
    //纵轴方向被划分的个数
    crossAxisCount: 2,
    //item的数量
    itemCount: _shopList.length,
    mainAxisSpacing: 10.0,
    crossAxisSpacing: 10.0,
    staggeredTileBuilder: (index) => StaggeredTile.fit(1),
    itemBuilder: (BuildContext context, int index) => GestureDetector(
    onTap: () async{
    final _app = 'amzn://'; /// 跳转到亚马逊app
    final _url = _shopList[index].url;
    if(await canLaunch(_app)) {
    await launch(_app);
    } else {
    await canLaunch(_url) ? await launch(_url) : Chili.showToast('Could not launch amazon');
    }
    },
    child: Container(/// 商品
    decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(10.0),
    boxShadow: [
    BoxShadow(
    color: Color.fromRGBO(196, 208, 226, 0.15),
    offset: Offset(6.0, 6.0),
    blurRadius: 6.0,
    )
    ]
    ),
    child: Padding(
    padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 15.0),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.center,
    children: [
    SizedBox(
    width: 151.0,
    height: 151.0,
    /// 商品图片
    child: CachedNetworkImage(
    imageUrl: '${Global.imgPath}${_shopList[index].thumbnail}',
    fit: BoxFit.cover,
    errorWidget: (context, url, error) => Icon(Icons.error),
    )
    ),
    /// 名称
    Padding(
    padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 10.0),
    child: Text(_shopList[index].name, textAlign: TextAlign.center,style: TextStyle(color: Color(0xFF333333), fontSize: 12.0)),
    ),
    /// 价格
    Text('\$${_shopList[index].price}', style: TextStyle(color: Color(0xFFFF89B6), fontSize: 14.0, letterSpacing: -0.01))
    ],
    ),
    ),
    ),
    )
    ),
    + +

    25.解决基线对齐

      +
    1. +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Row(
    crossAxisAlignment: CrossAxisAlignment.baseline,
    textBaseline: TextBaseline.alphabetic,
    children: [
    Text('反反复复:', style: TextStyle(fontSize: 40.0),),
    SizedBox(width: 10.0,),
    Text('ff', style: TextStyle(fontSize: 40.0),)
    ],
    )
    + +
      +
    1. 给 Text 设置高度
    2. +
    +

    26.图片裁剪

    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
    /// ----------------------------主要逻辑----------------------------
    final cropKey = GlobalKey<CropState>();
    /// 处理图片
    Future _handleImage(File originalImage) async{
    final crop = cropKey.currentState;
    final scale = crop?.scale;
    final area = crop?.area;
    late File handeledImg;
    if(area == null ) {
    Chili.showToast('Save failed');
    return;
    }
    /// 请求访问照片的权限
    bool result = await ImageCrop.requestPermissions();
    if (result) {
    try {
    handeledImg = await ImageCrop.cropImage(
    file: originalImage,
    scale: scale,
    area: area
    );
    } catch(e) {
    Chili.showToast(e.toString());
    Nav.pop();
    }
    }else {
    handeledImg = originalImage; /// 失败则获取原来的图片路径
    }
    return handeledImg.path;
    }


    /// 返回处理完图片的路径
    _imgPath = await _handleImage(File(widget.image));
    + +

    27. 选取图片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /// ----------------------------主要逻辑----------------------------
    final ImagePicker _picker = ImagePicker();
    XFile? image; /// 定义图片类型变量
    bool result = await ShowModal.showText(
    context,
    title: 'Select your operation',
    confirm: 'Camera',
    cancel: 'Album'
    );
    if(result) {
    image = await _picker.pickImage(source: ImageSource.camera, imageQuality: 40);
    } else {
    image = await _picker.pickImage(source: ImageSource.gallery, imageQuality: 40);
    }
    /// 如果用户有选取图片则路由到截图
    if(image != null) {
    Nav.push((context) => CropImageScreen(image!.path));
    print('path: ${image.path}');
    }
    + +

    28. flutter_blue

      +
    1. flutter_blue api

      +
      1
      2
      3
      4
      5
      6
      7
      8
      flutterBlue = FlutterBlue.instance.state

      flutterBlue.state
      flutterBlue.startScan()
      flutterBlue.connectedDevices
      flutterBlue.scanResults
      flutterBlue.isScanning
      flutterBlue.stopScan()
      +
    2. +
    3. BluetoothState api

      +
      1
      2
      3
      4
      5
      6
      7
      8
      9
      enum BluetoothState {
      unknown,
      unavailable,
      unauthorized,
      turningOn,
      on,
      turningOff,
      off
      }
      +
    4. +
    5. BluetoothDeviceState

      +
      1
      enum BluetoothDeviceState { disconnected, connecting, connected, disconnecting }
    6. +
    +

    29.自定义底部弹框

    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
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    import 'package:flutter/material.dart';
    import 'package:chili/chili.dart';

    class ShowModal {
    static Future<bool> showText(context, {title = '', text = '', confirm: 'confirm', cancel = 'cancel'}) async{
    return showModalBottomSheet(
    context: context,
    enableDrag: false,
    isDismissible: false,
    backgroundColor: Colors.white,
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.only(
    topLeft: Radius.circular(30.0),
    topRight: Radius.circular(30.0),
    )
    ),
    builder: (BuildContext context) {
    return SingleChildScrollView(
    child: Container(
    padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 15.0),
    child: Column(
    children: [
    /// 标题
    Text(title, style: TextStyle(color: Color(0xFF333333), fontSize: 16.0)),
    /// 文本
    text.isNotEmpty ? Container(
    margin: EdgeInsets.symmetric(vertical: 15.0),
    child: Text(text, textAlign: TextAlign.center, style: TextStyle(color: Color(0xFF999999), fontSize: 12.0),
    ),
    ) : Container(),
    Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
    /// 确认
    TextButton(
    onPressed: (){
    Nav.pop(true);
    },
    style: TextButton.styleFrom(
    backgroundColor: Color(0xFF5BCD49),
    padding: EdgeInsets.zero,
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(13.5)/// 外边框
    ),
    minimumSize: Size.zero
    ),
    child: Container(
    width: 70.0,
    padding: EdgeInsets.symmetric(vertical: 5.0),
    child: Center(child: Text(confirm, style: TextStyle(color: Colors.white, fontSize: 12.0))),
    )
    ),
    /// 取消
    TextButton(
    onPressed: (){
    Nav.pop(false);
    },
    style: TextButton.styleFrom(
    padding: EdgeInsets.zero,
    backgroundColor: Colors.white,
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(13.5),
    side: BorderSide(width: 0.5, color: Color(0xFF999999)) /// 外边框
    ),
    minimumSize: Size.zero
    ),
    child: Container(
    width: 70.0,
    padding: EdgeInsets.symmetric(vertical: 5.0),
    child: Center(child: Text(cancel, style: TextStyle(color: Color(0xFF999999), fontSize: 12.0))),
    )
    ),
    ],
    )
    ],
    ),
    ),
    );
    },
    ).then((value) => value);
    }
    }



    /// 调用
    bool result = await ShowModal.showText(
    context,
    title: 'Select your operation',
    confirm: 'Camera',
    cancel: 'Album'
    );
    + +

    30.常用正则表达式

    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
    /// 常用正则验证
    /// @author [Huangch]
    class CommonRegular {

    ///手机号
    static bool isPhone(String input) {
    RegExp mobile = new RegExp(r"1[0-9]\d{9}$");
    return mobile.hasMatch(input);
    }

    ///6~16位数字和字符组合
    static bool isLoginPassword(String input) {
    RegExp password = new RegExp(r"(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,16}$");
    return password.hasMatch(input);
    }

    ///手机号屏蔽中间4位
    static String shieldPhone(String phoneNUm) {
    return phoneNUm.replaceFirst(RegExp(r"\d{4}"), "****",3);
    }

    ///身份证
    static bool isNationalId(String input) {
    RegExp id = RegExp(r'^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$');
    return id.hasMatch(input);
    }


    }
    + +

    31.关于 ?

    1
    slide[index]?.image != null  // ?. 如果slide[index]为null,则前半部分都为null
    + +

    32.常用的登录表单布局

    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
    child: Form(
    child: Column(
    children: [
    TextFormField(
    decoration: InputDecoration(
    prefixIconConstraints: BoxConstraints(
    maxWidth: 15.0,
    maxHeight: 18.0
    ),
    prefixIcon: Image.asset('icon_login_phone'.webp,
    fit: BoxFit.cover),
    hintStyle: TextStyle(
    color: Color(0xFFBFBFBF), fontSize: 14.0),
    hintText: '请输入手机号/邮箱'),
    ),
    TextFormField(
    decoration: InputDecoration(
    contentPadding: EdgeInsets.only(left: 20.0),
    prefixIconConstraints: BoxConstraints(
    maxWidth: 15.0,
    maxHeight: 18.0
    ),
    prefixIcon: Image.asset(
    'icon_login_yanzhengma'.webp,
    fit: BoxFit.cover,
    ),
    border: UnderlineInputBorder(
    borderSide: BorderSide(
    color: Color(0xFFF5F6F7)
    )
    ),
    hintText: '请输入验证码'))
    ],
    ),
    )
    + +

    32.网络图片缓存组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    CachedNetworkImage(
    width: 78.0,
    height: 78.0,
    imageUrl: 'https://gitee.com/littleJshao/figure-bed/raw/master/images/dragon-1.jpg',
    progressIndicatorBuilder:
    (context, url, downloadProgress) =>
    CupertinoActivityIndicator(),
    fit: BoxFit.cover,
    errorWidget: (context, url, error) =>
    Icon(Icons.error),
    )
    + +

    33.解决华为手机在连接 Android Studio 调试时出现异常:Error while Launching activity

    百思不得其解,查找多方原因后才发现原来的应用被我从手机上卸载了,但是 Android Studio 却发现你的应用没卸载干净,导致两个应用签名不一致,所以在安装应用的时候会报出无法找到 MainActivity 入口的异常。

    +

    在终端中输入以下命令:

    +

    adb uninstall yourPakageName

    +

    博客地址

    +

    34.CustomScrollView 之 Sliver 家族的 Widget

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    CustomScrollView buildCustomScrollView() {
    return CustomScrollView(
    ///反弹效果
    physics: BouncingScrollPhysics(),
    ///Sliver 家族的 Widget
    slivers: <Widget>[
    ///复杂的标题
    buildSliverAppBar(),
    ///间距
    SliverPadding(
    padding: EdgeInsets.all(5),
    ),

    ///九宫格
    buildSliverGrid(),
    ///间距
    SliverPadding(
    padding: EdgeInsets.all(5),
    ),
    ///列表
    buildSliverFixedExtentList()
    ],
    );
    }
    + +

    35.Flutter Wrap 流式布局嵌套循环 Row 导致占据一行问题

    项目中我们在使用 Wrap 去循环数据的时候,有一些 UI 需要使用到 Row 布局来进行展示,但是众所周知的是,Row 布局会占满一行,这就导致我们的 Wrap 失效了,如何解决呢?

    +
    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
    Wrap(
    spacing:ScreenAdapter.setWidth(20),// 主轴(水平)方向间距
    runSpacing:ScreenAdapter.setHeight(13), // 纵轴(垂直)方向间距
    // alignment: WrapAlignment.start, //沿主轴方向居中
    direction:Axis.horizontal,
    children: tagList.map<Widget>((item){
    return InkWell(
    child: Container(
    padding: EdgeInsets.symmetric(horizontal:ScreenAdapter.setWidth(20), vertical:ScreenAdapter.setHeight(10)),
    decoration: BoxDecoration(
    color:Color(0xffEEEEEE),
    borderRadius: BorderRadius.circular(20)
    ),
    child:
    RichText(
    text: TextSpan(
    style: TextStyle(fontSize: 25, color: Colors.black, fontWeight:FontWeight.w500),
    children: [
    WidgetSpan(
    child: Container(
    width: 30,
    child: AspectRatio(
    aspectRatio: 1/1,
    child: ClipRRect(
    borderRadius: BorderRadius.circular(20),
    child: Container(
    color:Colors.white,
    child:Icon(Icons.add, size: 20, color:Color(0xffDB4739))
    ),
    ),
    ),
    ),
    ),
    TextSpan(text:'${item['title']}')
    ]
    ),
    )
    ),
    onTap: (){
    Navigator.pushNamed(context, '/themeDetails', arguments: {'id':'3RDOl99mWb'});
    },
    );
    }).toList()
    )
    + +

    36.flutter 输入框 TextField 设置高度以及背景色等样式的正确姿势

    37.Row 中 使用 Text,并实现超出隐藏

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Row(
    mainAxisAlignment: MainAxisAlignment.end,
    children: [
    Flexible(
    child: Text(
    '${(value?.toString()?.isEmpty ?? true) ? '请选择' : value.toString()}',
    overflow: TextOverflow.ellipsis,
    style: TextStyle(
    fontSize: 14.0, color: Color(0xFF999999))),
    ),
    Image.asset('btn_common_right'.webp,
    width: 28.0, height: 28.0, fit: BoxFit.cover)
    ],
    )
    + +

    38.DropdownButton

    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
    DropdownButton(
    alignment: AlignmentDirectional.centerEnd,
    items: List.generate(
    _goodsType.length,
    (index) => DropdownMenuItem(
    child: Text(_goodsType[index].name!),
    value: _goodsType[index].id,
    ),
    ),
    value: goodsCategory.id,
    isDense: true,
    style: TextStyle(
    fontSize: 14.0, color: Color(0xFF333333)),
    onChanged: (val) {
    setter(() {
    goodsCategory.id = val as int;
    });
    },
    icon: Image.asset(
    'btn_common_down'.webp,
    width: 28.0,
    height: 29.0,
    fit: BoxFit.cover,
    ),
    underline: Container(),
    )
    + +

    39.复杂列表的使用,并保持销毁,初始化

    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
    CustomScrollView(
    slivers: [
    SliverList(
    delegate: SliverChildListDelegate([
    GoogleMapScreen(
    latitude: 2.0, longitude: 2.0, onTap: (pos) {})
    ]),
    ),
    SliverList(
    delegate: SliverChildBuilderDelegate(
    (context, index) {
    List<String> images = _list[index].images!.split(';');
    return DataCard(
    info: [
    labelText('仓库名称:', '${_list[index].name}'),
    labelText('仓库地址:', '${_list[index].address}'),
    labelText('仓库电话:', '${_list[index].contactsPhone}'),
    labelText('当前距离::', '${_list[index].distance}'),
    ],
    imageWrap: images,
    btnGroup: [
    customButton(text: '一键导航', callback: () {}),
    customButton(text: '拨打电话', callback: () {}),
    ],
    );
    },
    childCount: _list.length,
    ))
    ],
    )
    + +

    40.类似 label 标签,带有外边框

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Container(
    margin: EdgeInsets.only(right: 8.0),
    padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0),
    decoration: BoxDecoration(
    color: Color.fromRGBO(255, 159, 33, 0.08),
    border: Border.all(color: Color(0xFFFF9F21)),
    borderRadius: BorderRadius.all(Radius.circular(4.0))),
    child: Text('${text}',
    style: TextStyle(color: Color(0xFFFF9F21), fontSize: 12.0)),
    )
    + +

    41.安卓内部更新 ota_update

    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
      void _updateVersion() async{
    if (Platform.isIOS){
    String url = '${widget.data.url}'; // 这是微信的地址,到时候换成自己的应用的地址
    if (await canLaunch(url)){
    await launch(url);
    }else {
    throw 'Could not launch $url';
    }
    }else if (Platform.isAndroid){
    String url = '${widget.data.url}';
    print('下载的链接:${widget.data.url}');
    try {
    // destinationFilename 是对下载的apk进行重命名
    OtaUpdate().execute(url, destinationFilename: 'news.apk').listen(
    (OtaEvent event) {
    print('status:${event.status},value:${event.value}');
    switch(event.status){
    case OtaStatus.DOWNLOADING:// 下载中
    setState(() {
    progress = '下载进度:${event.value}%';
    });
    break;
    case OtaStatus.INSTALLING: //安装中
    print('安装中');
    progress=null;
    setState(() {});
    break;
    case OtaStatus.PERMISSION_NOT_GRANTED_ERROR: // 权限错误
    print('更新失败,请稍后再试');
    break;
    default: // 其他问题
    print('其他问题');
    break;
    }
    },
    );
    } catch (e) {
    print('更新失败,请稍后再试');
    }
    }
    }


    ///api--版本更新检查
    ApplicationFindLatestData data;
    _applicationFindLatest(String platform) async {
    await RequestUtil.applicationFindLatest(
    NoActionIntercept(this),
    platform,
    ).then((res) {
    if(res.code==200){
    data = res.data;
    Commons.isCheckUpData = true;
    print('现在版本号:${LocalStorage.get(Commons.VERSION)} 目标:${data.name}');
    if (data.name != LocalStorage.get(Commons.VERSION)) {
    // Commons.isShowUpData = true;
    showDialog(context: context,builder: (context){
    return UpdateApp(data: data,);
    });
    }else{
    showToast('已是最新版本');
    }
    setState(() {});
    }
    }).catchError((e) {});
    }
    + +

    43.打开外部链接 url_launcher

      +
    1. android 配置
    2. +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <queries>
    <!-- If your app opens https URLs -->
    <intent>
    <action android:name="android.intent.action.VIEW" />
    <data android:scheme="https" />
    </intent>
    <!-- If your app makes calls -->
    <intent>
    <action android:name="android.intent.action.DIAL" />
    <data android:scheme="tel" />
    </intent>
    <!-- If your app emails -->
    <intent>
    <action android:name="android.intent.action.SEND" />
    <data android:mimeType="*/*" />
    </intent>
    </queries>

    + +
      +
    1. ios 配置

      +
      1
      2
      3
      4
      5
      <key>LSApplicationQueriesSchemes</key>
      <array>
      <string>https</string>
      <string>http</string>
      </array>
      +
    2. +
    3. 使用

      +
      1
      2
      void _launchURL() async =>
      await canLaunch(_url) ? await launch(_url) : throw 'Could not launch $_url';
    4. +
    +

    ==使用注意:url 要加特定的前缀;例如:‘tel’+url 调用拨号界面==

    +

    44.flutter 打包的 app 闪退

    在 app 下面的 build.gradle 中:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //关闭混淆, 否则在运行release包后可能出现运行崩溃, TODO后续进行混淆配置
    minifyEnabled false //删除无用代码
    shrinkResources false //删除无用资源



    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 //删除无用代码
    shrinkResources false //删除无用资源
    }
    }
    + +

    46. ios 加载(loading)

    1
    CupertinoActivityIndicator()
    + +

    47.flutter 查看 pdf 文件

    1
    2
    #A package to show Native PDF View for iOS and Android, support Open from a different resource like Path, Asset or Url and Cache it.
    flutter_cached_pdfview
    + +

    48.轮播

    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
    CarouselSlider(
    /// 轮播
    carouselController: _controller,
    options: CarouselOptions(
    autoPlay: true,
    viewportFraction: 1,
    height: 281.0,
    onPageChanged: (index, res) {
    setState(() {
    _currBanner = index;
    });
    }
    ),
    items: bannerList
    .map((data) =>
    Builder(builder: (BuildContext context) {
    return GestureDetector(
    onTap: () {
    if(data.link!.isNotEmpty) {
    Utils.openUrl(data.link!);
    return;
    }
    Nav.push(
    (context) => BannerDetailScreen(
    title: data.title!,
    bannerId: data.id!,
    ));
    },
    ///控制图片显示的尺寸主要正对下面的CachedNetworkImage修改即可,例如图片无法撑开宽度则设置width:double.maxFinite,
    child: CachedNetworkImage(
    width: double.maxFinite,
    imageUrl:
    '${Global.baseImageUrl}${data.image}',
    progressIndicatorBuilder:
    (context, url, downloadProgress) =>
    CupertinoActivityIndicator(),
    height: 281.0,
    fit: BoxFit.cover,
    errorWidget: (context, url, error) =>
    Icon(Icons.error),
    ),
    );
    }))
    )
    + +

    49.谷歌路线图

    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
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    import 'package:flutter/material.dart';
    import 'package:chili/chili.dart';
    import 'package:flutter_polyline_points/flutter_polyline_points.dart';
    import 'package:google_maps_flutter/google_maps_flutter.dart';

    /// 查看司机位置
    class CheckDriverPosition extends StatefulWidget {
    const CheckDriverPosition({required this.destLocation, required this.originLocation, Key? key}) : super(key: key);

    final Map originLocation;
    final Map destLocation;

    @override
    State<CheckDriverPosition> createState() => _CheckDriverPositionState();
    }

    class _CheckDriverPositionState extends State<CheckDriverPosition> {

    GoogleMapController? _mapController;

    /// 标记点列表
    Map<MarkerId, Marker> markers = <MarkerId, Marker>{};

    ///初始化视野 的经纬度
    double centerLat = 0 ;
    double centerLong = 0;

    Map<PolylineId, Polyline> polylines = {};
    List<LatLng> polylineCoordinates = [];
    PolylinePoints polylinePoints = PolylinePoints();
    String googleApiKey = 'AIzaSyAEF80-c_mLIj7PxKi6XU8qlkAvvH3fbhM';

    @override
    void initState() {
    // TODO: implement initState
    super.initState();
    // /// origin marker
    _addMarker(LatLng(widget.originLocation["lat"], widget.originLocation["long"]), "origin",
    BitmapDescriptor.defaultMarker);
    /// destination marker
    _addMarker(LatLng(widget.destLocation["lat"], widget.destLocation["long"]), "destination",
    BitmapDescriptor.defaultMarker);
    _getPolyline();
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    backgroundColor: Color(0xffF9F9F9),
    appBar: AppBar(
    leading: IconButton(
    onPressed: () {
    Nav.pop();
    },
    icon: Image.asset('btn_common_left'.webp,
    width: 28.0, fit: BoxFit.cover),
    ),
    title: Text(
    '查看司机位置'.i18n,
    style: TextStyle(fontSize: 18),
    ),
    ),
    body: GoogleMap(
    zoomControlsEnabled: false,
    polylines: Set<Polyline>.of(polylines.values),
    initialCameraPosition: CameraPosition(
    zoom: 10.0,
    target: LatLng(widget.originLocation['lat'], widget.originLocation['long']),
    ),
    onMapCreated: _onMapCreated,
    myLocationEnabled: true,
    // markers: Set<Marker>.of(markers.values),
    tiltGesturesEnabled: true,
    compassEnabled: true,
    ));
    }

    void _onMapCreated(GoogleMapController controller) async {
    _mapController = controller;
    }

    _addMarker(LatLng position, String id, BitmapDescriptor descriptor) {
    MarkerId markerId = MarkerId(id);
    Marker marker =
    Marker(markerId: markerId, icon: descriptor, position: position);
    markers[markerId] = marker;
    }

    _addPolyLine() {
    PolylineId id = PolylineId("poly");
    Polyline polyline = Polyline(
    polylineId: id,
    color: Color(0xFFFF9F21),
    points: polylineCoordinates,
    width: 5,
    );
    polylines[id] = polyline;
    setState(() {});
    }

    _getPolyline() async {
    await polylinePoints
    .getRouteBetweenCoordinates(
    googleApiKey,
    PointLatLng(widget.originLocation['lat'], widget.originLocation['long']),
    PointLatLng(widget.destLocation['lat'], widget.destLocation['long']),
    wayPoints: [
    PolylineWayPoint(location: "22.802306,113.164728"),
    PolylineWayPoint(location: "22.557069, 113.429766"),
    ],
    travelMode: TravelMode.driving,
    ) .then((value) {
    if (value.points.isNotEmpty) {
    value.points.forEach((PointLatLng point) {
    polylineCoordinates.add(LatLng(point.latitude, point.longitude));
    });
    } else {}
    _addPolyLine();
    });
    }

    ///获取起点与终点之间 中间的经纬度坐标
    // void getCenterLonLat(){
    // centerLat = widget.originLocation['lat'] - widget.destLocation['lat'];
    // centerLong = widget.originLocation['long'] - widget.destLocation['long'];
    // //Math.abs()绝对值
    // if( centerLong > 0){
    // centerLong = widget.originLocation['long'] - centerLong.abs() / 2;
    // }else{
    // centerLong = widget.destLocation['long'] - centerLong.abs() / 2;
    // }
    // if( centerLat > 0){
    // centerLat = widget.originLocation['lat'] - centerLat.abs() / 2;
    // }else{
    // centerLat = widget.destLocation['lat'] - centerLat.abs() / 2;
    // }
    // }

    }

    + +

    50.下载文件的 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
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    import 'dart:isolate';
    import 'dart:ui';
    import 'dart:async';
    import 'dart:io';
    import 'package:device_info_plus/device_info_plus.dart';
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:chili/chili.dart';
    import 'package:flutter_downloader/flutter_downloader.dart';
    import 'package:path_provider/path_provider.dart';
    import 'package:permission_handler/permission_handler.dart';
    import 'package:ting_hui_wu_liu_user_app/global.dart';

    class DownloadFileWidget extends StatefulWidget {
    const DownloadFileWidget({required this.link, Key? key}) : super(key: key);

    /// 下载链接
    final String link;

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

    class _DownloadFileWidgetState extends State<DownloadFileWidget> {

    /// 当前的任务
    late TaskInfo _task;

    /// 侦听器
    ReceivePort _port = ReceivePort();

    /// 保存路径
    late String _localPath;

    /// 绑定前台和后台之间的通信
    void _bindBackgroundIsolate() {
    bool isSuccess = IsolateNameServer.registerPortWithName(
    _port.sendPort, 'downloader_send_port');
    if (!isSuccess) {
    _unbindBackgroundIsolate();
    _bindBackgroundIsolate();
    return;
    }
    _port.listen((dynamic data) {
    // if (debug) {
    // print('UI Isolate Callback: $data');
    // }
    String? id = data[0];
    DownloadTaskStatus? status = data[1];
    int? progress = data[2];

    // if (_tasks != null && _tasks!.isNotEmpty) {
    // final task = _tasks!.firstWhere((task) => task.taskId == id);
    // setState(() {
    // task.status = status;
    // task.progress = progress;
    // });
    // }
    });
    }
    /// 检查权限
    Future<bool> _checkPermission() async {
    DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
    AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
    if (Theme.of(context).platform == TargetPlatform.android &&
    androidInfo.version.sdkInt! <= 28) {
    final status = await Permission.storage.status;
    if (status != PermissionStatus.granted) {
    final result = await Permission.storage.request();
    if (result == PermissionStatus.granted) {
    return true;
    }
    } else {
    return true;
    }
    } else {
    return true;
    }
    return false;
    }

    /// 设置保存路径
    Future<String?> _findLocalPath() async {
    var externalStorageDirPath;
    if (Platform.isAndroid) {
    final directory = await getExternalStorageDirectory();
    externalStorageDirPath = directory?.path;
    } else if (Platform.isIOS) {
    externalStorageDirPath =
    (await getApplicationDocumentsDirectory()).absolute.path;
    }
    // print('externalStorageDirPath:\n$externalStorageDirPath');
    return externalStorageDirPath;
    }

    Future<void> _prepareSaveDir() async {
    _localPath = (await _findLocalPath())!;
    final savedDir = Directory(_localPath);
    bool hasExisted = await savedDir.exists();
    if (!hasExisted) {
    savedDir.create();
    }
    }

    /// 初始化存储路径,存储权限,获取本地所有任务信息
    Future<void> _prepare() async{
    /// 获取本地的所有任务信息
    final List<DownloadTask> ?tasks = await FlutterDownloader.loadTasks();
    tasks!.forEach((item) {
    if(item.url == _task.link) {
    _task.taskId = item.taskId;
    _task.status = item.status;
    _task.progress = item.progress;
    }
    });

    if(await _checkPermission()) {
    await _prepareSaveDir();
    }
    }

    /// 下载的回调
    static void downloadCallback(
    String id, DownloadTaskStatus status, int progress) {
    // if (debug) {
    // print(
    // 'Background Isolate Callback: task ($id) is in status ($status) and process ($progress)');
    // }
    final SendPort send =
    IsolateNameServer.lookupPortByName('downloader_send_port')!;
    send.send([id, status, progress]);
    }

    /// 销毁前后台之间的通信
    void _unbindBackgroundIsolate() {
    IsolateNameServer.removePortNameMapping('downloader_send_port');
    }

    /// 请求下载
    void _requestDownload(TaskInfo task) async {
    task.taskId = await FlutterDownloader.enqueue(
    url: task.link!,
    headers: {"auth": "test_for_sql_encoding"},
    savedDir: _localPath,
    showNotification: true,
    openFileFromNotification: true,
    saveInPublicStorage: true,
    );
    }

    /// 重新下载
    void _retryDownload(TaskInfo task) async {
    String? newTaskId = await FlutterDownloader.retry(taskId: task.taskId!);
    task.taskId = newTaskId;
    }

    /// 打开文件
    Future<bool> _openDownloadedFile(TaskInfo? task) {
    if (task != null) {
    return FlutterDownloader.open(taskId: task.taskId!);
    } else {
    return Future.value(false);
    }
    }

    @override
    void initState() {
    // TODO: implement initState
    super.initState();
    _task = TaskInfo(link: "${Global.baseImageUrl}${widget.link}");
    _bindBackgroundIsolate();

    FlutterDownloader.registerCallback(downloadCallback);

    _prepare();
    }

    @override
    Widget build(BuildContext context) {
    return Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
    Container(
    decoration: BoxDecoration(
    color: const Color(0xFFFF9F21),
    borderRadius: BorderRadius.circular(5.0)
    ),
    padding: const EdgeInsets.symmetric(horizontal: 42.0, vertical: 5.0),
    child: const Text('付款请求书.pdf', style: TextStyle(color: Colors.white, fontSize: 14.0),),
    ),
    TextButton(
    style: TextButton.styleFrom(
    padding: EdgeInsets.zero,
    minimumSize: Size.zero,
    tapTargetSize: MaterialTapTargetSize.shrinkWrap,
    ),
    onPressed: (){

    },
    child: _buildActionForTask(_task),
    )
    ],
    );
    }

    Widget _buildActionForTask(TaskInfo task) {
    if(task.status == DownloadTaskStatus.running) {/// 下载中
    return CircularProgressIndicator(
    value: task.progress! / 100,
    backgroundColor: Colors.white,
    valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFFF9F21)),
    );
    } else if(task.status == DownloadTaskStatus.failed) {/// 下载失败
    return IconButton(
    onPressed: () => _retryDownload(task),
    icon: Icon(Icons.refresh_rounded, size: 16.0,)
    );
    } else if(task.status == DownloadTaskStatus.complete) {
    return TextButton(
    onPressed: () => _openDownloadedFile(task),
    child: Text('打开', style: TextStyle(color: Color(0xFFFF9F21), fontSize: 14.0))
    );
    } else if (task.status == DownloadTaskStatus.enqueued) {/// 等待
    return CupertinoActivityIndicator();
    }
    return TextButton(
    onPressed: () => _retryDownload(task),
    child: Text('下载', style: TextStyle(color: Color(0xFFFF9F21), fontSize: 14.0))
    );
    }
    }

    /// 自定义任务类型数据
    class TaskInfo {
    /// 下载链接
    final String? link;

    String? taskId;
    int? progress = 0;
    DownloadTaskStatus? status = DownloadTaskStatus.undefined;

    TaskInfo({this.link});
    }
    import 'dart:isolate';
    import 'dart:ui';
    import 'dart:async';
    import 'dart:io';
    import 'package:device_info_plus/device_info_plus.dart';
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:chili/chili.dart';
    import 'package:flutter_downloader/flutter_downloader.dart';
    import 'package:path_provider/path_provider.dart';
    import 'package:permission_handler/permission_handler.dart';
    import 'package:ting_hui_wu_liu_user_app/global.dart';

    class DownloadFileWidget extends StatefulWidget {
    const DownloadFileWidget({required this.link, Key? key}) : super(key: key);

    /// 下载链接
    final String link;

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

    class _DownloadFileWidgetState extends State<DownloadFileWidget> {

    /// 当前的任务
    late TaskInfo _task;

    /// 侦听器
    ReceivePort _port = ReceivePort();

    /// 保存路径
    late String _localPath;

    /// 绑定前台和后台之间的通信
    void _bindBackgroundIsolate() {
    bool isSuccess = IsolateNameServer.registerPortWithName(
    _port.sendPort, 'downloader_send_port');
    if (!isSuccess) {
    _unbindBackgroundIsolate();
    _bindBackgroundIsolate();
    return;
    }
    _port.listen((dynamic data) {
    // if (debug) {
    // print('UI Isolate Callback: $data');
    // }
    String? id = data[0];
    DownloadTaskStatus? status = data[1];
    int? progress = data[2];

    // if (_tasks != null && _tasks!.isNotEmpty) {
    // final task = _tasks!.firstWhere((task) => task.taskId == id);
    // setState(() {
    // task.status = status;
    // task.progress = progress;
    // });
    // }
    });
    }
    /// 检查权限
    Future<bool> _checkPermission() async {
    DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
    AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
    if (Theme.of(context).platform == TargetPlatform.android &&
    androidInfo.version.sdkInt! <= 28) {
    final status = await Permission.storage.status;
    if (status != PermissionStatus.granted) {
    final result = await Permission.storage.request();
    if (result == PermissionStatus.granted) {
    return true;
    }
    } else {
    return true;
    }
    } else {
    return true;
    }
    return false;
    }

    /// 设置保存路径
    Future<String?> _findLocalPath() async {
    var externalStorageDirPath;
    if (Platform.isAndroid) {
    final directory = await getExternalStorageDirectory();
    externalStorageDirPath = directory?.path;
    } else if (Platform.isIOS) {
    externalStorageDirPath =
    (await getApplicationDocumentsDirectory()).absolute.path;
    }
    // print('externalStorageDirPath:\n$externalStorageDirPath');
    return externalStorageDirPath;
    }

    Future<void> _prepareSaveDir() async {
    _localPath = (await _findLocalPath())!;
    final savedDir = Directory(_localPath);
    bool hasExisted = await savedDir.exists();
    if (!hasExisted) {
    savedDir.create();
    }
    }

    /// 初始化存储路径,存储权限,获取本地所有任务信息
    Future<void> _prepare() async{
    /// 获取本地的所有任务信息
    final List<DownloadTask> ?tasks = await FlutterDownloader.loadTasks();
    tasks!.forEach((item) {
    if(item.url == _task.link) {
    _task.taskId = item.taskId;
    _task.status = item.status;
    _task.progress = item.progress;
    }
    });

    if(await _checkPermission()) {
    await _prepareSaveDir();
    }
    }

    /// 下载的回调
    static void downloadCallback(
    String id, DownloadTaskStatus status, int progress) {
    // if (debug) {
    // print(
    // 'Background Isolate Callback: task ($id) is in status ($status) and process ($progress)');
    // }
    final SendPort send =
    IsolateNameServer.lookupPortByName('downloader_send_port')!;
    send.send([id, status, progress]);
    }

    /// 销毁前后台之间的通信
    void _unbindBackgroundIsolate() {
    IsolateNameServer.removePortNameMapping('downloader_send_port');
    }

    /// 请求下载
    void _requestDownload(TaskInfo task) async {
    task.taskId = await FlutterDownloader.enqueue(
    url: task.link!,
    headers: {"auth": "test_for_sql_encoding"},
    savedDir: _localPath,
    showNotification: true,
    openFileFromNotification: true,
    saveInPublicStorage: true,
    );
    }

    /// 重新下载
    void _retryDownload(TaskInfo task) async {
    String? newTaskId = await FlutterDownloader.retry(taskId: task.taskId!);
    task.taskId = newTaskId;
    }

    /// 打开文件
    Future<bool> _openDownloadedFile(TaskInfo? task) {
    if (task != null) {
    return FlutterDownloader.open(taskId: task.taskId!);
    } else {
    return Future.value(false);
    }
    }

    @override
    void initState() {
    // TODO: implement initState
    super.initState();
    _task = TaskInfo(link: "${Global.baseImageUrl}${widget.link}");
    _bindBackgroundIsolate();

    FlutterDownloader.registerCallback(downloadCallback);

    _prepare();
    }

    @override
    Widget build(BuildContext context) {
    return Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
    Container(
    decoration: BoxDecoration(
    color: const Color(0xFFFF9F21),
    borderRadius: BorderRadius.circular(5.0)
    ),
    padding: const EdgeInsets.symmetric(horizontal: 42.0, vertical: 5.0),
    child: const Text('付款请求书.pdf', style: TextStyle(color: Colors.white, fontSize: 14.0),),
    ),
    TextButton(
    style: TextButton.styleFrom(
    padding: EdgeInsets.zero,
    minimumSize: Size.zero,
    tapTargetSize: MaterialTapTargetSize.shrinkWrap,
    ),
    onPressed: (){

    },
    child: _buildActionForTask(_task),
    )
    ],
    );
    }

    Widget _buildActionForTask(TaskInfo task) {
    if(task.status == DownloadTaskStatus.running) {/// 下载中
    return CircularProgressIndicator(
    value: task.progress! / 100,
    backgroundColor: Colors.white,
    valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFFF9F21)),
    );
    } else if(task.status == DownloadTaskStatus.failed) {/// 下载失败
    return IconButton(
    onPressed: () => _retryDownload(task),
    icon: Icon(Icons.refresh_rounded, size: 16.0,)
    );
    } else if(task.status == DownloadTaskStatus.complete) {
    return TextButton(
    onPressed: () => _openDownloadedFile(task),
    child: Text('打开', style: TextStyle(color: Color(0xFFFF9F21), fontSize: 14.0))
    );
    } else if (task.status == DownloadTaskStatus.enqueued) {/// 等待
    return CupertinoActivityIndicator();
    }
    return TextButton(
    onPressed: () => _retryDownload(task),
    child: Text('下载', style: TextStyle(color: Color(0xFFFF9F21), fontSize: 14.0))
    );
    }
    }

    /// 自定义任务类型数据
    class TaskInfo {
    /// 下载链接
    final String? link;

    String? taskId;
    int? progress = 0;
    DownloadTaskStatus? status = DownloadTaskStatus.undefined;

    TaskInfo({this.link});
    }
    + +

    51.WidgetSpan

    在文本中内嵌固定大小 Widget。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    RichText(
    text: TextSpan(
    children: [
    WidgetSpan(
    child: Image.asset(
    '${_currIndex == index ? 'btn_login_choose_s' : 'btn_login_choose_d'}'.webp,
    width: 18.0,
    height: 18.0,
    fit: BoxFit.cover,
    ),
    ),
    WidgetSpan(
    child: Padding(
    padding: EdgeInsets.only(left: 5.0),
    child: Text('${widget.group[index]}'.i18n, style: TextStyle(fontSize: 14.0),),
    ),
    )
    ]
    ),
    )
    + +

    52.打开 google 应用的相关 api

    53.聚焦

    关于焦点事件:https://www.freesion.com/article/4272635917/

    +
    1
    2
    3
    ///输入框的焦点
    FocusNode _focusNode = FocusNode();
    FocusScope.of(context).requestFocus(FocusNode());
    + +

    54.Flutter 文本输入框 TextField 属性

    57.下拉刷新,上滑加载

    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
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    import 'dart:async';
    import 'dart:io';

    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:pull_to_refresh/pull_to_refresh.dart';
    import 'package:chili/chili.dart';
    import 'package:ting_hui_wu_liu_user_app/models/model.dart';

    /// 自定义下拉刷新
    class PullDownRefresh<T> extends StatefulWidget {
    const PullDownRefresh({required this.controller, required this.future, required this.pageable, required this.builder, this.noDataText, this.noMoreDataText, Key? key}) : super(key: key);

    final Future<T> Function(Pageable pageable) future;
    final Widget Function(BuildContext context, T value) builder;
    final RefreshController controller;
    final Pageable pageable;
    /// 没有更多的数据
    final String? noMoreDataText;
    /// 没有数据
    final String? noDataText;

    @override
    _PullDownRefreshState<T> createState() => _PullDownRefreshState();
    }

    class _PullDownRefreshState<T> extends State<PullDownRefresh<T>> {
    /// 数据
    T? list;
    /// 列表是否为空
    bool isNoData = true;

    bool enableLoad = false;

    String err = '';

    @override
    void initState() {
    // TODO: implement initState
    super.initState();
    }

    void _handleError(dynamic e){
    String? message;
    widget.controller.refreshFailed();
    if (e is SocketException) {
    message = 'network.error'.i18n;
    logger.e(message, e);
    } else if (e is RpcCallException) {
    if (e.code == 401) {
    Nav.login();
    } else if(e.code == 600) {
    String language = LocalStorage.getLocale()?.languageCode ?? 'zh';
    message = e.fault?.variables[language];
    } else {
    message = e.message.i18n;
    logger.e(message, e);
    }
    } else if (e is HttpCallException) {
    message = 'http.error'.i18n;
    logger.e(message, e);
    } else if (e is TimeoutException) {
    message = 'request.timeout'.i18n;
    logger.e(message, e);
    } else {
    message = 'request.error'.i18n;
    logger.e(message, e);
    }
    if (message != null) {
    err = message;
    Chili.showToast(message);
    }
    }

    @override
    Widget build(BuildContext context) {
    return SmartRefresher(
    controller: widget.controller,
    enablePullDown: true,
    enablePullUp: enableLoad,
    header: WaterDropMaterialHeader(
    color: Color(0xFFFF9F21),
    ),
    footer: ClassicFooter(
    noDataText: widget.noMoreDataText ?? "--没有更多的数据--",
    loadingIcon: CupertinoActivityIndicator(),
    ),
    onRefresh: () {
    widget.controller.loadComplete();
    err = '';
    widget.pageable.page = 1;
    widget.future(widget.pageable).then((value) {
    list = value;
    widget.controller.refreshCompleted();
    if(mounted) {
    setState(() {
    if ((list as List).isEmpty) {
    isNoData = true;
    enableLoad = false;
    } else {
    isNoData = false;
    enableLoad = true;
    }
    });
    }
    }).catchError((e) {
    _handleError(e);
    });
    },
    onLoading: () {
    widget.pageable.page = widget.pageable.page! + 1;
    widget.future(widget.pageable).then((value) {
    if ((value as List).isEmpty) {
    widget.controller.loadNoData();
    } else {
    if(mounted) setState(() {(list as List).addAll(value);});
    widget.controller.loadComplete();
    }
    }).catchError((err) {
    _handleError(err);
    widget.controller.loadFailed();
    });
    },
    child: isNoData ? Center(
    child: Text(
    err.isNotEmpty ? err : (widget.noDataText ?? '暂无数据'),
    style: TextStyle(color: Color(0xFFBFBFBF)),
    ),
    ) : widget.builder(context, list!)
    );
    }
    }


    + +

    IntrinsicHeight

    将其子控件调整为该子控件的固有高度,举个例子来说,Row 中有 3 个子控件,其中只有一个有高度,默认情况下剩余 2 个控件将会充满父组件,而使用 IntrinsicHeight 控件,则 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
    class TextWrapper extends StatefulWidget {
    const TextWrapper(this.text, {Key? key}) : super(key: key);

    final String text;

    @override
    State<TextWrapper> createState() => _TextWrapperState();
    }

    class _TextWrapperState extends State<TextWrapper>
    with TickerProviderStateMixin {
    bool isExpanded = false;

    @override
    Widget build(BuildContext context) {
    return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    AnimatedSize(
    vsync: this,
    duration: Duration(microseconds: 300),
    child: ConstrainedBox(
    constraints:
    isExpanded ? BoxConstraints() : BoxConstraints(maxHeight: 70),
    child: Text(
    widget.text,
    style: TextStyle(fontSize: 16),
    softWrap: true,
    overflow: TextOverflow.fade,
    ),
    ),
    ),
    isExpanded
    ? Row(
    // 使用 Row 将 btn 显示在右边,如果不使用 Row,btn 就会显示在左边
    mainAxisAlignment: MainAxisAlignment.end,
    children: [
    OutlinedButton(
    onPressed: () {
    setState(() {
    isExpanded = false;
    });
    },
    child: Text("隐藏"))
    ],
    )
    : OutlinedButton(
    onPressed: () {
    setState(() {
    isExpanded = true;
    });
    },
    child: Text("显示"))
    ],
    );
    }
    }
    + +
    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
    class TextWrapperPage extends StatelessWidget {
    const TextWrapperPage({Key? key}) : super(key: key);

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text("显示/折叠"),
    centerTitle: true,
    backgroundColor: Colors.blue,
    ),
    body: Padding(
    padding: EdgeInsets.symmetric(vertical: 30, horizontal: 20),
    child: SingleChildScrollView(
    child: Column(
    children: [
    TextWrapper(
    "【摘要】 食品安全问题关系国计民生,一直是社会各界广泛关注的焦点。基于政策法规、主流期刊、权威媒体的三维视角,首先从\"是什么\"的角度对改革开放四十年以来我国食品安全问题关注重点的变化进行了系统梳理,总体上,我国食品安全问题关注重点的变化轨迹可描绘为\"食品数量安全→食品数量和卫生安全→食品质量安全→食品质量和营养安全\";其次进一步从\"为什么\"的角度剖析不同历史阶段我国食品安全问题关注重点变迁的内在逻辑,揭示导致以上变化的主要驱动因素;最后总结改革开放以来我国食品安全领域的重要成就,指明我国食品安全问题的发展方向。 "),
    Divider(
    height: 30,
    ),
    TextWrapper(
    "【摘要】 食品安全问题关系国计民生,一直是社会各界广泛关注的焦点。基于政策法规、主流期刊、权威媒体的三维视角,首先从\"是什么\"的角度对改革开放四十年以来我国食品安全问题关注重点的变化进行了系统梳理,总体上,我国食品安全问题关注重点的变化轨迹可描绘为\"食品数量安全→食品数量和卫生安全→食品质量安全→食品质量和营养安全\";其次进一步从\"为什么\"的角度剖析不同历史阶段我国食品安全问题关注重点变迁的内在逻辑,揭示导致以上变化的主要驱动因素;最后总结改革开放以来我国食品安全领域的重要成就,指明我国食品安全问题的发展方向。 "),
    ],
    ),
    )),
    );
    }
    }
    + +

    58.简单的单例模式写法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class TestEventBust {
    static TestEventBust _instance = TestEventBust._init();
    /// 命名构造函数
    TestEventBust._init();
    EventBus _eventBus = EventBus();
    EventBus get bus{
    return _eventBus;
    }
    /// 工厂构造函数
    factory TestEventBust() => _instance;
    }
    + +

    50.BackdropFilter 高斯模糊/毛玻璃效果

    Flutter 自带的一个 ui 组件。

    +

    注意点:
    官方文档:The filter will be applied to all the area within its parent or ancestor widget’s clip. If there’s no clip, the filter will be applied to the full screen.

    +

    译:过滤器将应用于其父控件或祖先控件剪辑中的所有区域。如果没有剪辑,过滤器将应用于全屏。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    Stack(
    fit: StackFit.expand,
    children: <Widget>[
    Text('0' * 10000),
    Center(
    child: ClipRect( // <-- clips to the 200x200 [Container] below
    child: BackdropFilter(
    filter: ui.ImageFilter.blur(
    sigmaX: 5.0,
    sigmaY: 5.0,
    ),
    child: Container(
    alignment: Alignment.center,
    width: 200.0,
    height: 200.0,
    child: const Text('Hello World'),
    ),
    ),
    ),
    ),
    ],
    )
    + +

    51.显示 SVG 格式的 Flutter 组件:flutter_svg

    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
    ListView.builder(
    shrinkWrap: true,
    scrollDirection: Axis.vertical,
    itemCount: countries.length,
    physics: ScrollPhysics(),
    itemBuilder: (context, index){
    final Widget networkSvg = SvgPicture.network(
    '${countries[index].flag}',
    fit: BoxFit.fill,
    semanticsLabel: 'A shark?!',
    placeholderBuilder: (BuildContext context) => Container(
    padding: const EdgeInsets.all(30.0),
    child: const CircularProgressIndicator(
    backgroundColor: Colors.redAccent,
    )),);
    return
    Column(
    children: [
    ListTile(
    title: Text('${countries[index].name}'),
    leading: CircleAvatar(
    backgroundColor: Colors.white,
    child: networkSvg,
    ),
    )
    ],
    );
    });
    + +

    Flutter shape

    新了解:shapeDecoration

    +

    关于形状

    +

    通过屏幕密度选择对应尺寸的图片

    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
    import 'dart:ui';

    import 'package:chili/chili.dart';
    import 'package:flutter/material.dart';

    import 'main_screen.dart';

    class IntroScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    double dpi = MediaQueryData.fromWindow(window).devicePixelRatio; //屏幕密度
    String prefix = '';
    final imgWidth = MediaQuery.of(context).size.width;
    if (dpi < 1.5) {
    prefix = "assets/intro/small";
    } else if (dpi < 2) {
    prefix = "assets/intro/medium";
    } else {
    prefix = "assets/intro/large";
    }
    List<Widget> pages = [
    Image.asset(
    '$prefix/1.png',
    fit: BoxFit.fill,
    ),
    Image.asset(
    '$prefix/2.png',
    fit: BoxFit.fill,
    ),
    Image.asset(
    '$prefix/3.png',
    fit: BoxFit.fill,
    ),
    Image.asset(
    '$prefix/4.png',
    fit: BoxFit.fill,
    ),
    ];
    return Introduction(
    onSkip: () {
    Nav.pushReplacement((context) => MainScreen());
    },
    pages: pages,
    next: Text('Next'),
    skip: Text('Skip'),
    );
    }
    }

    + +
    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
    static init() {
    print("当前的屏幕密度为:$dpi");
    if (platform == 1) {
    print("当前设备为Android");
    postfix = ".png";
    if (dpi < 1) {
    basePath = Local_Icon_prefix + "android/mdpi/";
    } else if (dpi < 1.5) {
    basePath = Local_Icon_prefix + "android/hdpi/";
    } else if (dpi < 2) {
    basePath = Local_Icon_prefix + "android/xhdpi/";
    } else if (dpi < 3) {
    basePath = Local_Icon_prefix + "android/xxhdpi/";
    } else {
    basePath = Local_Icon_prefix + "android/xxxhdpi/";
    }
    } else {
    basePath = Local_Icon_prefix + "ios/";
    if (dpi < 2.5) {
    postfix = "@2x.png";
    } else {
    postfix = "@3x.png";
    }
    }
    print(basePath);
    return basePath;
    }
    + +
    + + +
    +
    +
      +
    • Title: Flutter常用组件
    • +
    • Author: Jared Yuan
    • +
    • Created at + : 2023-09-22 12:00:12
    • + +
    • + Updated at + : 2023-09-22 12:07:14 +
    • + +
    • + Link: https://redefine.ohevan.com/2023/09/22/Flutter/Flutter常用组件/ +
    • +
    • + + License: + + + This work is licensed under CC BY-NC-SA 4.0. + + +
    • +
    +
    + +
    + + + + + + + + + +
    + +
    + +
    + + +
    + +
    + +
    + + + + +
    +
    +
    +
    +  Comments +
    + + + + + +
    + + + + + + +
    + +
    + +
    + + +
    +
    +
    +
    On this page
    +
    Flutter常用组件
    +
    +
    +
    + +
    + + + + + +
    + + + +
    + + +
    + + +
    +
    +
      + + +
    • + +
    • + + + + +
    • + +
    • + +
    +
    + +
    + + +
    +
    +
      +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + + + + + + +
    • + +
    • +
    + +
      +
    • + +
    • + +
    • + + +
    • + + +
    +
    + +
    + +
    + +
    + + + +
    + +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + +
    + + + + diff --git "a/2023/09/22/Flutter/\347\250\200\346\234\211\347\273\204\344\273\266/index.html" "b/2023/09/22/Flutter/\347\250\200\346\234\211\347\273\204\344\273\266/index.html" new file mode 100644 index 0000000..9e750d9 --- /dev/null +++ "b/2023/09/22/Flutter/\347\250\200\346\234\211\347\273\204\344\273\266/index.html" @@ -0,0 +1,894 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 稀有组件 - + + Jshao's Motel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + +
    + + +
    + + + +
    + + +
    + + + +
    + +
    + + + +
    + + +
    +
    + +
    + +

    稀有组件

    + +
    + + + + +
    +
    + +
    +
    +
    + Jared Yuan + + Lv2 + +
    +
    + + +
    +
    +
    + + + + + +
    +

    InteractiveViewer

    A widget that enables pan and zoom interactions with its child.

    +

    Document

    + +
    + + +
    +
    +
      +
    • Title: 稀有组件
    • +
    • Author: Jared Yuan
    • +
    • Created at + : 2023-09-22 11:53:32
    • + +
    • + Updated at + : 2023-09-22 12:00:35 +
    • + +
    • + Link: https://redefine.ohevan.com/2023/09/22/Flutter/稀有组件/ +
    • +
    • + + License: + + + This work is licensed under CC BY-NC-SA 4.0. + + +
    • +
    +
    + +
    + + + + + + + + + +
    + +
    + +
    + + +
    + +
    + +
    + + + + +
    +
    +
    +
    +  Comments +
    + + + + + +
    + + + + + + +
    + +
    + +
    + + +
    +
    +
    +
    On this page
    +
    稀有组件
    + + +
    +
    +
    + +
    + + + + + +
    + + + +
    + + +
    + + +
    +
    +
      + + +
    • + +
    • + + + + +
    • + +
    • + +
    +
    + +
    + + +
    +
    +
      +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + + + + + + +
    • + +
    • +
    + +
      +
    • + +
    • + +
    • + + +
    • + + +
    +
    + +
    + +
    + +
    + + + +
    + +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + +
    + + + + diff --git a/404.html b/404.html index c9dccb0..7050705 100644 --- a/404.html +++ b/404.html @@ -73,7 +73,7 @@ @@ -227,7 +227,7 @@
  • - PORTFOLIO + 复盘-1
  • @@ -342,7 +342,7 @@
  • - PORTFOLIO + 复盘-1
  • diff --git a/about/index.html b/about/index.html index 69dfb24..b01e613 100644 --- a/about/index.html +++ b/about/index.html @@ -75,7 +75,7 @@ @@ -229,7 +229,7 @@
  • - PORTFOLIO + 复盘-1
  • @@ -344,7 +344,7 @@
  • - PORTFOLIO + 复盘-1
  • diff --git a/archives/2023/09/index.html b/archives/2023/09/index.html index 8f29804..c73e177 100644 --- a/archives/2023/09/index.html +++ b/archives/2023/09/index.html @@ -73,7 +73,7 @@ @@ -227,7 +227,7 @@
  • - PORTFOLIO + 复盘-1
  • @@ -342,7 +342,7 @@
  • - PORTFOLIO + 复盘-1
  • @@ -372,10 +372,46 @@
    2023 - [2] + [8]
    - +
    diff --git a/index.html b/index.html index 6e81404..94c30e5 100644 --- a/index.html +++ b/index.html @@ -71,7 +71,7 @@ @@ -317,7 +317,7 @@

  • - PORTFOLIO + 复盘-1
  • @@ -432,7 +432,7 @@

  • - PORTFOLIO + 复盘-1
  • @@ -490,20 +490,20 @@

    Jared Yuan
    -
    Lv1
    +
    Lv2
    @@ -529,6 +529,387 @@

    +

    + + Flutter 内购实现 + +

    + +
    + +

    基于 in_app_purchase 封装的内购方法

    + + +
    + + + + + +
  • + + + + + + + +

    + + Flutter 进阶 + +

    + +
    + +

    开发中进阶操作

    + + +
    + + + +
  • + +
  • + + + + + + + +

    + + Dart学习 + +

    + +
    + +

    Dart 语法、细节、注意点

    + + +
    + + + +
  • + +
  • + + + + + + + +

    + + Flutter常用组件 + +

    + +
    + +

    记录Flutter中自定义实现的组件

    + + +
    + + + +
  • + +
  • + + + + + + + +

    + + 稀有组件 + +

    + +
    + + InteractiveViewerA widget that enables pan and zoom interactions with its child. +Document + + +
    + + + +
  • + +
  • + + + + + + + +

    + + Dart小记 + +

    + +
    + +

    Dart 随手记

    + + +
    + + + +
  • + +
  • + + + + + + +

    Flutter State-01 @@ -537,7 +918,8 @@

    - +

    Flutter State 状态管理篇-01

    +
    diff --git a/search.xml b/search.xml index 9e6b9ea..c9ace86 100644 --- a/search.xml +++ b/search.xml @@ -1,9 +1,95 @@ + + Dart学习 + /2023/09/22/Dart%E5%AD%A6%E4%B9%A0/ +
    (一)Duration 是有正负之分

    1、获取常用的参数:

    import 'Test/Test.dart';

    //Duration类型的使用demo
    main() {
    //定义一个Duration
    Duration stim = Duration(
    days: 0, hours: 1, minutes: 0,
    seconds: 30, milliseconds: 000, microseconds: 00
    );
    //常用操作
    print("获取天数值:${stim.inDays}"); //获取days的值:0
    print("获取总的秒数值:${stim.inSeconds}"); //返回总的秒数,整个时间转换为秒数
    print("获取小时数值:${stim.inMinutes}"); //只会返回小时的值,小时后面的分钟会被忽略
    print(stim.toString()); //转换为字符串
    print(stim.isNegative); //返回此“Duration”是否为负
    print(stim.abs()); //获取Duration绝对值:abs()
    }
    + +

    2、比较两个 Duration 类型的大小,可以应用于比较两个视频的时间长短等等。

    +
    import 'Test/Test.dart';

    main() {
    //定义一个Duration
    Duration stim = Duration(
    days: 0, hours: 1, minutes: 0,
    seconds: 30, milliseconds: 000, microseconds: 00
    );
    //定义一个Duration
    Duration stim1=Duration(
    days: 0,hours: 0,minutes: 0, seconds:30,milliseconds: 000,microseconds: 00
    );
    //将stim与stim1比较,等于则返回0,大于返回1,小于返回-1
    print(stim.compareTo(stim1)); //结果:1
    }
    + +

    3、Duration 类型的常用比较,如两个 Duration 相加,相减等。

    import 'Test/Test.dart';

    //Duration类型的使用demo
    main() {
    //定义一个Duration
    Duration stim = Duration(
    days: 0, hours: 0, minutes: 0,
    seconds: 30, milliseconds: 000, microseconds: 00
    );
    //定义一个Duration
    Duration stim1=Duration(
    days: 0,hours: 0,minutes: 0, seconds:30,milliseconds: 000,microseconds: 00
    );

    print(stim+stim1); //两个Duration相加,结果:0:01:00.000000
    print(stim-stim1); //两个Duration相减,结果:0:00:00.000000
    print(stim*2); //乘 ,结果:0:01:00.000000
    print(stim ~/100); //除,注意有个波浪号~,结果:0:00:00.003000
    }
    + +

    4、Duration 类型截取指定部份显示。

    通过上面的示例不难看出一个问题就是 Duration 显示的时间跨度是带有小数点的,如(0:01:00.000000)。而且显示的是小数点后面 6 位,在实际的应用中,我们只需要显示时:分: 秒的格式(如:0:01:30)。或者像视频进度条里显示的格式(01:30)。那么如何实现这种效果呢?实现方式如下

    +
     获取Duration-->转为字符串-->利用字符串的截取方法截取指定的位置字符串。
    //Duration类型的使用demo
    main() {
    //定义一个Duration
    Duration stim = Duration(
    days: 0, hours: 0, minutes: 60, seconds: 30, milliseconds: 40, microseconds: 50
    );
    print(stim); //结果:1:00:30.040050
    String str=stim.toString(); //原始数据转为字符串
    print(str.substring(2,7)); //截取指定位置的数据,结果:00:30
    print(str.substring(0,7)); //截取指定位置的数据,结果:1:00:30
    }
    + +

    (二)Timer 定时器

    import 'dart:async';
    + +
    Timer(
    Duration duration,
    void callback()
    )
    + +

    构造方法:

    const timeout = const Duration(seconds: 5);
    print('currentTime ='+DateTime.now().toString());
    Timer(timeout, () {
    //5秒后执行
    print('after 5s Time ='+DateTime.now().toString());
    });
    + +

    Timer.periodic执行多次回调:
    回调多次的定时器用法和回调一次的差不多,区别有下面两点:
    1、API 调用不同
    2、需要手动取消,否则会一直回调,因为是周期性的

    +
    z`t count = 0;
    const period = const Duration(seconds: 1);
    print('currentTime='+DateTime.now().toString());
    Timer.periodic(period, (timer) {
    //到时回调
    print('afterTimer='+DateTime.now().toString());
    count++;
    if (count >= 5) {
    //取消定时器,避免无限回调
    timer.cancel();
    timer = null;
    }
    });

    + +
    import 'dart:async';

    main() {
    int count = 0;
    Timer timer;
    Timer.periodic(Duration(seconds: 1), (timer) {
    if (count == 5) {
    timer.cancel();
    return;
    }
    count++;
    print(count);
    });
    }

    + +

    (三)时间转换

    ///时间转换:
    void transformTime(int times) {
    int date = 0, hour = 0, minute = 0, second = 0;
    if (times > 0) {
    date = times ~/ 86400;
    hour = ((times - 86400 * date) ~/ 3600);
    minute = (times - 86400 * date - hour * 3600) ~/ 60;
    second =
    ((times - 86400 * date - hour * 3600 - minute * 60) % 60).toInt();
    }
    if (times < 60) {
    timer = times < 10 ? ('00:' + '0${second}') : ('00:' + '${second}');
    } else if (times > 3600) {
    timer = '${hour}:' + '${minute}:' + '${second}';
    }
    }
    + +

    (四)字符串

    Dart 的 String 字符串的常用方法

    +]]> + + Dart + + + + Dart小记 + /2023/09/22/Dart%E5%B0%8F%E8%AE%B0/ + 泛型方法:
      +
    • dart 的泛型方法书写和 Java 的不太一样。
    • +
    +
      +
    1. Dart 的泛型方法
    2. +
    +
    T first<T>(List<T> ts) {
    // 进行一些初始工作或错误检查,然后...
    T tmp = ts[0];
    // 进行一些额外的检查或处理...
    return tmp;
    }
    + +

    Tips

    +

    表示这是一个泛型方法,T 是类型参数的名称,

    +
    + +
      +
    1. Java 的泛型方法
    2. +
    +
    <T> T first(List<T> ts) {
    // 进行一些初始工作或错误检查,然后...
    T tmp = ts.get(0);
    // 进行一些额外的检查或处理...
    return tmp;
    }
    <T> 位于方法返回类型之前,表示这是一个泛型方法,T 是类型参数的名称,可以替换成其他合法的标识符。
    + +

    Mixin(混入)

    Tips

    +

    作用:解决无需多重继承即可拥有功能方法
    混合使用 with 关键字,with 后面可以是 class、abstract class 和 mixin 的类型

    +
    + +
    // mixin 声明的类
    mixin a {

    }

    // 普通的类
    class b {

    }

    class c with a, b {}
    + +
      +
    • 混入(with)多类,遇到同名方法的情况,按照混入的顺序,后面的会覆盖前面
    • +
    • mixin 的类无法定义构造函数,所以一般会将需要 mixin 的类使用 mixin 关键字
    • +
    • 使用关键字 on 限定混入条件
    • +
    +
    class Person {
    eat(){
    print("eat");
    }
    }

    class Dog {

    }

    /// 使用关键字 on 限定 Code 只能被 Person 或者其子类 mixin
    /// 添加限定后,可以重写其方法, Code 重写 Person 的方法
    /// super 表示调用父类(Person)的方法。
    mixin Code on Person {
    @override
    eat(){
    super.eat();
    }
    }
    + +
      +
    • 混合后的类型是超类的子类型(类似多继承)
    • +
    +
    class F {

    }

    class G {

    }

    class H {

    }

    class FG extends H with F,G {}

    FG fg = FG();
    /_
    fg is F: true
    fg is G: true
    fg is H: true
    _/

    + +

      +
    • 调用非默认超类构造函数
    • +
    +
    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().
    Employee.fromJson(super.data) : super.fromJson() {
    print('in Employee');
    }
    }

    void main() {
    var employee = Employee.fromJson({});
    print(employee);
    // Prints:
    // in Person
    // in Employee
    // Instance of 'Employee'
    }
    + +
    class Vector2d {
    final double x;
    final double y;

    Vector2d(this.x, this.y);
    }

    class Vector3d extends Vector2d {
    final double z;

    // Forward the x and y parameters to the default super constructor like:
    // Vector3d(final double x, final double y, this.z) : super(x, y);
    Vector3d(super.x, super.y, this.z);
    }

    +]]>
    + + Dart + + + Dart + 小记 + +
    Hello World /2023/09/13/hello-world/ Welcome to Hexo ! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub .

    +

    Welcome to!

    This is my blog, sharing somthing interesting with you.

    Quick Start

    Create a new post

    $ hexo new "My New Post"

    More info: Writing

    @@ -18,10 +104,29 @@

    More info: Deployment

    ]]>
    + + Flutter 内购实现 + /2023/09/22/Flutter/Flutter-%E5%86%85%E8%B4%AD%E5%AE%9E%E7%8E%B0/ +
    import 'dart:async';
    import 'dart:io';
    import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
    import 'package:in_app_purchase_storekit/store_kit_wrappers.dart';
    import 'package:flutter/cupertino.dart';
    import 'package:in_app_purchase/in_app_purchase.dart';

    /// 适用于ios端的内购处理方法
    class InAppPurchaseUtils {
    late final InAppPurchase _inAppPurchase;
    late StreamSubscription<List<PurchaseDetails>> _subscription;
    late final Stream<List<PurchaseDetails>> purchaseUpdated;
    /// 产品id
    static const List<String> productIds = [];
    List<ProductDetails> _products = <ProductDetails>[];

    /// 初始化
    InAppPurchaseUtils.init() {
    _inAppPurchase = InAppPurchase.instance;
    purchaseUpdated = _inAppPurchase.purchaseStream;
    initStoreInfo();
    }

    /// 查询产品信息
    /// 返回的数据类型是ProductDetailsResponse
    /// 获取详细的产品数据:productDetailResponse.productDetails
    /// Example:
    /// if (productDetailResponse.error != null) {
    // setState(() {
    // _queryProductError = productDetailResponse.error!.message;
    // _isAvailable = isAvailable;
    // _products = productDetailResponse.productDetails;
    // _purchases = <PurchaseDetails>[];
    // _notFoundIds = productDetailResponse.notFoundIDs;
    // _consumables = <String>[];
    // _purchasePending = false;
    // _loading = false;
    // });
    // return;
    // }
    Future<ProductDetailsResponse> queryProductDetails() async{
    return await _inAppPurchase.queryProductDetails(productIds.toSet());
    }

    List<ProductDetails> get products => _products;

    /// 初始化商品(ios端,未集成Android)
    Future<void> initStoreInfo() async{
    if(await isAvailable) {
    if (Platform.isIOS) {
    final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition =
    _inAppPurchase
    .getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
    await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());
    }
    queryProductDetails().then((value) {
    if(value.error != null) {
    _products = value.productDetails;
    }
    });
    }
    }

    /// 内购监听
    void addPurchaseListener({required PurchaseListener listener, ValueChanged? onError}) {
    _subscription = purchaseUpdated.listen((List<PurchaseDetails> purchaseDetailsList) {
    _listenToPurchaseUpdated(purchaseDetailsList, listener: listener);
    },onDone: (){
    _subscription.cancel();
    },onError: (Object err){
    if(onError != null) onError(err);
    });
    }

    /// 购买消耗产品(金币)
    void buyConsumable(ProductDetails productDetails){
    if(Platform.isIOS) {
    _inAppPurchase.buyConsumable(purchaseParam: PurchaseParam(productDetails: productDetails));
    }
    }

    /// 订阅
    void buyNonConsumable(ProductDetails productDetails){
    if(Platform.isIOS) {
    _inAppPurchase.buyNonConsumable(purchaseParam: PurchaseParam(productDetails: productDetails));
    }
    }

    /// 处理内购
    Future<void> _listenToPurchaseUpdated(
    List<PurchaseDetails> purchaseDetailsList, {required PurchaseListener listener}) async {
    for (final PurchaseDetails purchaseDetails in purchaseDetailsList) {
    if (purchaseDetails.status == PurchaseStatus.pending) {
    if(listener.onPending != null) listener.onPending!();
    // showPendingUI();
    } else {
    if(listener.onPendingComplete != null) listener.onPendingComplete!();
    if (purchaseDetails.status == PurchaseStatus.error) {
    if(listener.onError != null) listener.onError!(purchaseDetails.error);
    } else if (purchaseDetails.status == PurchaseStatus.purchased ||
    purchaseDetails.status == PurchaseStatus.restored) {
    listener.onPurchased(purchaseDetails);
    // final bool valid = await _verifyPurchase(purchaseDetails);
    // if (valid) {
    // deliverProduct(purchaseDetails);
    // } else {
    // _handleInvalidPurchase(purchaseDetails);
    // return;
    // }
    }
    // if (Platform.isAndroid) {
    // if (!_kAutoConsume && purchaseDetails.productID == _kConsumableId) {
    // final InAppPurchaseAndroidPlatformAddition androidAddition =
    // _inAppPurchase.getPlatformAddition<
    // InAppPurchaseAndroidPlatformAddition>();
    // await androidAddition.consumePurchase(purchaseDetails);
    // }
    // }
    if (purchaseDetails.pendingCompletePurchase) {
    await _inAppPurchase.completePurchase(purchaseDetails);
    }
    }
    }
    }

    /// 内购服务是否可用
    Future<bool> get isAvailable async => await _inAppPurchase.isAvailable();

    /// 取消内购服务监听
    /// dispose 时可调用
    void cancel(){
    if (Platform.isIOS) {
    final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition =
    _inAppPurchase
    .getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
    iosPlatformAddition.setDelegate(null);
    }
    _subscription.cancel();
    }
    }

    class PurchaseListener {
    /// 等待
    VoidCallback? onPending;
    ValueChanged? onError;
    /// 购买事件
    late ValueChanged<PurchaseDetails> onPurchased;
    ///等待结束
    VoidCallback? onPendingComplete;
    PurchaseListener({required this.onPurchased,this.onPending,this.onError,this.onPendingComplete});
    }

    class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper {
    @override
    bool shouldContinueTransaction(
    SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) {
    return true;
    }

    @override
    bool shouldShowPriceConsent() {
    return false;
    }
    }
  • +]]> + + Flutter + + + Flutter + in_app_purchase + + Flutter State-01 /2023/09/13/Flutter/Flutter_state_01/ - +

    参考

    +
    + +

    自定义 Provider

    class CustomProvider<T extends Listenable> extends StatefulWidget {
    final T Function() create;
    final Widget child;
    const CustomProvider({Key? key, required this.child, required this.create}) : super(key: key);

    @override
    State<CustomProvider> createState() => _CustomState<T>();

    static T of<T>(BuildContext context,{bool listen = true}) {
    if(listen) {
    return context.dependOnInheritedWidgetOfExactType<CustomInheritedWidget<T>>()!.model;
    } else {
    return (context.getElementForInheritedWidgetOfExactType<CustomInheritedWidget<T>>()!.widget as CustomInheritedWidget).model;
    }

    }
    }

    class _CustomState<T extends Listenable> extends State<CustomProvider<T>> {

    late T model;

    @override
    void initState() {
    // TODO: implement initState
    super.initState();
    model = widget.create();
    }

    @override
    Widget build(BuildContext context) {
    return AnimatedBuilder(
    animation: model,
    builder: (BuildContext context, Widget? child) {
    return CustomInheritedWidget(model: model,
    child: widget.child);
    },
    );
    }
    }


    class CustomInheritedWidget<T> extends InheritedWidget {
    final T model;

    const CustomInheritedWidget({super.key, required this.model,required super.child});

    static CustomInheritedWidget of<T>(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<CustomInheritedWidget<T>>()!;
    }

    @override
    bool updateShouldNotify(covariant InheritedWidget oldWidget) {
    print("update");
    return true;
    }
    }

    /// extension如果没有取名字只能在本文件中使用,属于私有扩展。起名字可以供其他外部使用
    extension Consumer on BuildContext {
    T watch<T>() => CustomProvider.of(this);
    T
    +]]>
    Flutter @@ -30,4 +135,235 @@ 状态管理
    + + 稀有组件 + /2023/09/22/Flutter/%E7%A8%80%E6%9C%89%E7%BB%84%E4%BB%B6/ + InteractiveViewer

    A widget that enables pan and zoom interactions with its child.

    +

    Document

    +]]>
    + + Flutter + +
    + + Flutter 进阶 + /2023/09/22/Flutter/Flutter-%E8%BF%9B%E9%98%B6/ + 1.provider

    https://blog.csdn.net/lpfasd123/article/details/101573980

    +

    2.InheritedWidget


    + +

    3.provider

    Provider,Google 官方推荐的一种 Flutter 页面状态管理组件,它的实质其实就是对 InheritedWidget 的包装,使它们更易于使用和重用。

    +
      +
    • 创建一个 ChangeNotifier

      +
      class Model1 extends ChangeNotifier {
      int _count = 1;
      int get value => _count;
      set value(int value) {
      _count = value;
      notifyListeners();
      }
      }
      +
    • +
    • 创建一个 ChangeNotifier(方式一)

      +

      这里通过 ChangeNotifierProvider 的 create 把 ChangeNotifier(即 Model1)建立联系,作用域的范围在 child 指定的 MaterialApp,这里我们将 SingleStatsView 作为首页,SingleStatsView 里面使用了 Model1 作为数据源。需要注意的是,不要把所有状态的作用域都放在 MaterialApp,根据实际业务需求严格控制作用域范围,全局状态多了会严重影响应用的性能。

      +
      return ChangeNotifierProvider(
      create: (context) {
      return Model1();
      },
      child: MaterialApp(
      theme: ArchSampleTheme.theme,
      home: SingleStatsView(),
      ),
      );

      class SingleStatsView extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
      return Center(
      child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
      FlatButton(
      color: Colors.blue,
      child: Text('Model1 count++'),
      onPressed: () {
      Provider.of<Model1>(context, listen: false).count++;
      },
      ),
      Padding(
      padding: const EdgeInsets.only(bottom: 8.0),
      child: Text('Model count值变化监听',
      style: Theme.of(context).textTheme.title),
      ),
      Padding(
      padding: const EdgeInsets.only(bottom: 8.0),
      child: Text('Model1 count:${Provider.of<Model1>(context).count}',
      style: Theme.of(context)
      .textTheme
      .subhead
      .copyWith(color: Colors.green)),
      ),
      ],
      ),
      );
      }
      }
      +
    • +
    • 创建一个 ChangeNotifierProvider.value(方式二)

      +
      return ChangeNotifierProvider.value(
      value: Model1(),
      child: MaterialApp(
      theme: ArchSampleTheme.theme,
      home: SingleStatsView(),
      ));
      +
    • +
    • 在页面中监听状态变更,其他使用方式

      +

      ValueListenableBuilder

      +
    • +
    +

    Stream 的相关案例

    import 'dart:async';

    class DataBloc {

    factory DataBloc() => _instance;

    static final DataBloc _instance = DataBloc._init();

    ///定义一个Controller
    StreamController<String> _dataController = StreamController.broadcast(
    onListen: (){
    print('databloc listen');
    },
    onCancel: (){
    print('databloc cancel');
    }
    );
    ///获取 StreamSink 做 add 入口
    StreamSink<String> get dataSink => _dataController.sink;
    ///获取 Stream 用于监听
    Stream<String> get dataStream => _dataController.stream;
    ///事件订阅对象
    late StreamSubscription _dataSubscription;

    DataBloc._init() {
    ///监听事件
    _dataSubscription = dataStream.listen((value){
    ///do change
    });

    }

    close() {
    ///关闭
    _dataSubscription.cancel();
    _dataController.close();
    }
    }
    + +
    import 'dart:async';
    import 'dart:async';
    import 'dart:io';
    import 'package:logger/logger.dart';
    import 'package:amap_flutter_location/amap_flutter_location.dart';
    import 'package:amap_flutter_location/amap_location_option.dart';

    var logger = Logger(
    printer: PrettyPrinter(methodCount: 1, printEmojis: false, colors: false),
    );
    /// 基于定位设计的Bloc
    class LocationBloc {
    /// 实现单例模式
    factory LocationBloc() => _instance;

    static final LocationBloc _instance = LocationBloc._init();

    // static LocationBloc get instance => _instance;

    LocationBloc._init() {
    if (Platform.isIOS) {
    _requestAccuracyAuthorization();
    }
    ///监听事件
    _dataSubscription = _locationPlugin.onLocationChanged().listen((Map<String, Object>? result) {
    _dataController.sink.add(result);
    });
    _startLocation();


    }

    static AMapFlutterLocation _locationPlugin = new AMapFlutterLocation();
    ///定义一个Controller
    StreamController<Map<String, Object>?> _dataController = StreamController.broadcast();
    ///获取 StreamSink 做 add 入口
    // StreamSink<List<String>> get _dataSink => _dataController.sink;
    ///获取 Stream 用于监听
    Stream<Map<String, Object>?> get dataStream => _dataController.stream;
    ///事件订阅对象
    late StreamSubscription _dataSubscription;

    ///设置定位参数
    static void _setLocationOption() {
    if (null != _locationPlugin) {
    AMapLocationOption locationOption = new AMapLocationOption();

    ///是否单次定位
    locationOption.onceLocation = false;

    ///是否需要返回逆地理信息
    locationOption.needAddress = true;

    ///逆地理信息的语言类型
    locationOption.geoLanguage = GeoLanguage.DEFAULT;

    locationOption.desiredLocationAccuracyAuthorizationMode =
    AMapLocationAccuracyAuthorizationMode.ReduceAccuracy;

    locationOption.fullAccuracyPurposeKey = "AMapLocationScene";

    ///设置Android端连续定位的定位间隔(源码里面默认应该是2秒)
    locationOption.locationInterval = 10000;

    ///设置Android端的定位模式<br>
    ///可选值:<br>
    ///<li>[AMapLocationMode.Battery_Saving]</li>
    ///<li>[AMapLocationMode.Device_Sensors]</li>
    ///<li>[AMapLocationMode.Hight_Accuracy]</li>
    locationOption.locationMode = AMapLocationMode.Hight_Accuracy;

    ///设置iOS端的定位最小更新距离<br>
    locationOption.distanceFilter = -1;

    ///设置iOS端期望的定位精度
    /// 可选值:<br>
    /// <li>[DesiredAccuracy.Best] 最高精度</li>
    /// <li>[DesiredAccuracy.BestForNavigation] 适用于导航场景的高精度 </li>
    /// <li>[DesiredAccuracy.NearestTenMeters] 10米 </li>
    /// <li>[DesiredAccuracy.Kilometer] 1000米</li>
    /// <li>[DesiredAccuracy.ThreeKilometers] 3000米</li>
    locationOption.desiredAccuracy = DesiredAccuracy.Best;

    ///设置iOS端是否允许系统暂停定位
    locationOption.pausesLocationUpdatesAutomatically = false;

    ///将定位参数设置给定位插件
    _locationPlugin.setLocationOption(locationOption);
    }
    }

    ///获取iOS native的accuracyAuthorization类型
    static void _requestAccuracyAuthorization() async {
    AMapAccuracyAuthorization currentAccuracyAuthorization =
    await _locationPlugin.getSystemAccuracyAuthorization();
    if (currentAccuracyAuthorization ==
    AMapAccuracyAuthorization.AMapAccuracyAuthorizationFullAccuracy) {
    print("精确定位类型");
    } else if (currentAccuracyAuthorization ==
    AMapAccuracyAuthorization.AMapAccuracyAuthorizationReducedAccuracy) {
    print("模糊定位类型");
    } else {
    print("未知定位类型");
    }
    }

    ///开始定位
    static void _startLocation() {
    if (null != _locationPlugin) {
    ///开始定位之前设置定位参数
    _setLocationOption();
    _locationPlugin.startLocation();
    }
    }

    close() {
    ///关闭
    logger.d('移除定位订阅');
    _dataSubscription.cancel();
    _dataController.close();
    }
    }

    + +

    文章:https://cloud.tencent.com/developer/article/1511980

    +

    https://cloud.tencent.com/developer/article/1610790

    +
    class StateSubject {
    static final StateSubject _instance = StateSubject._();

    factory StateSubject() => StateSubject._instance;

    StreamController<int> streamController;

    StateSubject._() {
    streamController = StreamController.broadcast();
    }

    void update(int num) {
    streamController.sink.add(num);
    }
    }
    + +

    Flutter ios 设置中文语言

    1、先把手机的语言模式设置成简体中文

    +

    2、在 Info.Plist 里面把 Localization native development region 字段修改成 China

    +

    3、在 Info.Plist 里面添加字段 Localized resources can be mixed(Boolean)值为 YES

    +

    方法都设置好了后,打开相机调用的还是英文

    +

    还要在项目的 PROJECT -> Info -> Localizations 中添加语言包才可以
    ————————————————
    版权声明:本文为 CSDN 博主「moon 清泉」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/MoonAndroid/article/details/121625582

    +]]>
    + + Flutter + + + Flutter + +
    + + Flutter常用组件 + /2023/09/22/Flutter/Flutter%E5%B8%B8%E7%94%A8%E7%BB%84%E4%BB%B6/ + 1.圆角按钮
    TextButton(
    onPressed: (){
    print('取消订单');
    },
    style: TextButton.styleFrom(
    padding: EdgeInsets.zero,
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(16.0),
    side: BorderSide(width: 1.0, color: Color(0xFFC4C8CC)) /// 外边框
    ),
    minimumSize: Size.zero
    ),
    child: Container(
    padding: EdgeInsets.symmetric(horizontal: 18.0, vertical: 5.0),
    child: Text('取消订单', style: TextStyle(color: Color(0xFFA8ADB3), fontSize: 12.0)),
    )
    ),
    + +
    ClipRRect(
    borderRadius: BorderRadius.circular(16.5),
    child: ElevatedButton(
    onPressed: () {
    Nav.push((context) => CheckoutCounter());
    },
    style: ButtonStyle(
    textStyle: MaterialStateProperty.all(TextStyle(fontSize: 12.0)),
    backgroundColor: MaterialStateProperty.all(Color(0xFF3F5AFF)),
    padding: MaterialStateProperty.all(EdgeInsets.symmetric(vertical: 8.0, horizontal: 26.0))
    ),
    child: Text("提交订单"),
    ),
    )
    + +

    2.Text

    Text('待发货', style: TextStyle(color: Color(0xFF223359), fontSize: 14.0)),
    + +

    3.圆形 Container

    Container(
    height: 200,
    width: 200,
    decoration: BoxDecoration(
    image: DecorationImage(
    image: NetworkImage(
    'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg'),
    fit: BoxFit.cover,
    ),
    border: Border.all(
    color: Colors.blue,
    width: 2,
    ),
    shape: BoxShape.circle, /// 一般这个属性就够了
    ),
    )
    + +

    4. 圆形按钮

    CSDN:关于 Flutter 的 button 的按钮 ElevatedButton

    +
    InviteNumScreen
    + +
    ElevatedButton(
    child: Text('button'),
    onPressed: (){},
    style: ButtonStyle(
    padding: MaterialStateProperty.all(EdgeInsets.all(20.0)),
    textStyle: MaterialStateProperty.all(TextStyle(fontSize: 20)),
    shape: MaterialStateProperty.all(
    CircleBorder(
    side: BorderSide(
    color: Colors.deepOrangeAccent,
    width: 1.0
    )
    )
    )
    )
    )
    + +

    5.自定义对话框

      +
    1. 调用方式

      +
      bool result = await showDialog(context: context, builder: (context) => CustomDialog('确定退出紫鲸书苑?'));
      +
    2. +
    3. 自定义的对话框

      +
      return Dialog(
      child: Container(
      decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(10.0)
      ),
      height: 123.0,
      child: Column(
      children: [
      Container(
      padding: EdgeInsets.symmetric(vertical: 30.0),
      child: Center(
      child: Text(_message, style: TextStyle(color: Color(0xFF223359), fontSize: 16.0)),
      ),
      ),
      Expanded(
      child: Row(
      children: [
      Flexible(
      child: Container(
      decoration: BoxDecoration(
      border: Border(
      top: BorderSide(width: 1.0, color: Color(0xFFEBEDF0)),
      right: BorderSide(width: 1.0, color: Color(0xFFEBEDF0))
      ),
      ),
      child: SizedBox.expand(
      child: TextButton(
      onPressed: (){
      Nav.pop(false);
      },
      child: Text('取消', style: TextStyle(fontSize: 14.0 ,color: Color(0xFFA8ADB3))),
      ),
      ),
      ),
      ),
      Flexible(
      child: Container(
      decoration: BoxDecoration(
      border: Border(
      top: BorderSide(width: 1.0, color: Color(0xFFEBEDF0))
      ),
      ),
      child: SizedBox.expand(
      child: TextButton(
      onPressed: (){
      Nav.pop(true);
      },
      child: Text('确定', style: TextStyle(fontSize: 14.0 ,color: Color(0xFF223359))),
      ),
      ),
      ),
      )
      ],
      )
      )],
      ),
      ),
      );
    4. +
    +

    6.圆角按钮

    ElevatedButton(
    onPressed: (){
    print('保存');
    },
    style: ButtonStyle(
    shape: MaterialStateProperty.all(
    RoundedRectangleBorder(borderRadius: BorderRadius.circular(25.0))
    ),
    ),
    child: Text('保存')
    )


    ElevatedButton(
    onPressed: (){

    },
    style: ElevatedButton.styleFrom(
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5.0)),
    padding: EdgeInsets.symmetric(horizontal: 27.0, vertical: 7.0),
    ),
    child: Text('确定')
    )
    + +

    7.独占一行 TextFormField

    TextFormField(
    decoration: InputDecoration(
    contentPadding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 16.0),
    filled: true,
    fillColor: Colors.white,
    hintText: '请输入',
    hintStyle: TextStyle(color: Color(0xFFA8ADB3), fontSize: 14.0),
    helperText: '请不要超过16个字符,支持中英文、数字',
    helperStyle: TextStyle(color: Color(0xFFC4C8CC), fontSize: 13.0),
    border: InputBorder.none
    ),
    style: TextStyle(fontSize: 14.0),
    ),
    + +

    8.独占一行的圆角按钮

    Padding(
    padding: EdgeInsets.symmetric(horizontal: 40.0),
    child: Row(
    children: [
    Expanded(
    child: ElevatedButton(
    onPressed: (){
    print('保存');
    },
    style: ButtonStyle(
    backgroundColor: MaterialStateProperty.all(Color(0xFF3F5AFF)),
    padding: MaterialStateProperty.all(EdgeInsets.symmetric(vertical: 12.0)),
    shape: MaterialStateProperty.all(
    RoundedRectangleBorder(borderRadius: BorderRadius.circular(25.0))
    ),
    ),
    child: Text('保存', style: TextStyle(fontSize: 14.0))
    ),
    ),
    ],
    ),
    )
    + +

    9.文本输入框不限制行数,类似富文本

    TextFormField(
    maxLines: null,
    expands: true,
    style: TextStyle(fontSize: 13.0),
    decoration: InputDecoration(
    contentPadding: EdgeInsets.zero,
    hintText: '填写详细地址',
    hintStyle: TextStyle(fontSize: 13.0, color: Color(0xFFA8ADB3)),
    border: InputBorder.none
    ),
    )
    + +
    separatorBuilder: (BuildContext context, int index){
    return Divider(height: 2.0, color: Color(0xFFEBEBEB));
    },
    itemCount: _linkList.length,
    + +

    10.有关圆角设置

    decoration: BoxDecoration(
    borderRadius: BorderRadius.only(
    topLeft: Radius.circular(30.0),
    topRight: Radius.circular(30.0)
    ),
    color: Colors.white,
    ),
    + +

    11.有关 showModalBottomSheet

    解决圆角底部弹窗

    +

    在 showModalBottomSheet 里面的根容器设置成 SingleChildScrollView,即可实现高度根据内容自适应

    +
    showModalBottomSheet(/// 底部弹窗
    context: context,
    enableDrag: false,
    backgroundColor: Colors.white,
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.only(
    topLeft: Radius.circular(30.0),
    topRight: Radius.circular(30.0),
    )
    ),
    builder: (BuildContext context) {
    return SingleChildScrollView(
    child: Container(
    padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 15.0),
    child: Column(
    children: [
    Text('The cancellation of account', style: TextStyle(color: Color(0xFF333333), fontSize: 16.0)),
    Padding(
    padding: const EdgeInsets.symmetric(vertical: 15.0),
    child: Text('Please note: Once your account is logged out,'
    ' you will not be able to log in and use your account, '
    'and your rights and interests will be cleared and cannot be restored.'
    ' Are you sure to cancel your account?',
    style: TextStyle(color: Color(0xFF999999), fontSize: 12.0),
    ),
    ),
    Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
    TextButton(
    onPressed: (){
    print('取消订单');
    },
    style: TextButton.styleFrom(
    backgroundColor: Color(0xFF5BCD49),
    padding: EdgeInsets.zero,
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(13.5)/// 外边框
    ),
    minimumSize: Size.zero
    ),
    child: Container(
    padding: EdgeInsets.symmetric(horizontal: 13.0, vertical: 5.0),
    child: Text('confirm', style: TextStyle(color: Colors.white, fontSize: 12.0)),
    )
    ),
    TextButton(
    onPressed: (){
    print('取消订单');
    },
    style: TextButton.styleFrom(
    padding: EdgeInsets.zero,
    backgroundColor: Colors.white,
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(13.5),
    side: BorderSide(width: 0.5, color: Color(0xFF999999)) /// 外边框
    ),
    minimumSize: Size.zero
    ),
    child: Container(
    padding: EdgeInsets.symmetric(horizontal: 13.0, vertical: 5.0),
    child: Text('cancel', style: TextStyle(color: Color(0xFF999999), fontSize: 12.0)),
    )
    ),
    ],
    )
    ],
    ),
    ),
    );
    },
    );
    + +

    12.列表展示

    /// 列表展示
    ListView.separated(
    shrinkWrap: true,
    padding: EdgeInsets.zero,
    physics: NeverScrollableScrollPhysics(),
    separatorBuilder: (BuildContext context, int index) {
    return Divider(
    height: 1.0,
    );
    },
    itemBuilder: (context, index) {
    return ShopItem(_list[index]);
    },
    itemCount: _list.length,
    )
    + +

    13.Container 阴影

    boxShadow: [
    BoxShadow(
    color: Colors.black12,
    offset: Offset(0.0, 15.0), //阴影xy轴偏移量
    blurRadius: 15.0, //阴影模糊程度
    spreadRadius: 1.0 //阴影扩散程度
    )
    ],
    + +

    14.TextFormField 属性

    15.Flutter 的 showModalBottomSheet 输入框被弹出的键盘挡住

    16.页面滚动,性能优越的结构

      +
    1. Column -> Expanded -> ListView.builder
    2. +
    +

    17.圆角 TextFormField

    TextFormField(
    cursorColor: Color(0xFF5BCD49),
    decoration: InputDecoration(
    filled: true,
    fillColor: Colors.white,
    border: OutlineInputBorder(
    borderRadius: BorderRadius.circular(10.0),
    borderSide: BorderSide.none
    ),
    hintText: 'Enter a new binding mailbox',
    hintStyle: TextStyle(
    color: Color(0xFFD6D9DD), fontSize: 14.0)),
    ),
    + +

    18.固定在界面上的时间选择器

    相关链接:

    +

    https://blog.csdn.net/mengks1987/article/details/104580596

    +

    https://segmentfault.com/a/1190000020205762

    +

    19.ListWheelScrollView.useDelegate

    ListWheelScrollView.useDelegate(
    itemExtent: 50.0,
    diameterRatio: .5,
    childDelegate: ListWheelChildBuilderDelegate(
    builder: (context, index) {
    return Container(
    alignment: Alignment.center,
    color: Colors.primaries[index % 10],
    child: Text('$index'),
    );
    }
    ),
    /// 选中事件
    onSelectedItemChanged: (val){
    print(val);
    },
    )
    + +

    20.TextFormField 文章

    http://www.ptbird.cn/flutter-form-textformfield.html
    + +
    Column(
    children: [
    Flexible(
    child: ListView(
    shrinkWrap: true,
    children: _links.map((item) =>
    GestureDetector(
    onTap: (){
    Nav.push((context) => item['link']);
    },
    child: Container(
    margin: EdgeInsets.only(top: 5.0),
    height: 50.0,
    padding: EdgeInsets.only(left: 15.0, right: 17.0),
    color: Colors.white,
    child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
    Text(item['title'], style: TextStyle(color: Color(0xFF333333), fontSize: 14.0)),
    Image.asset('arrow_click'.webp, width: 6.0, height: 12.0, fit: BoxFit.cover)
    ],
    ),
    ),
    )
    ).toList(),
    ),
    ),
    ],
    )

    + +

    21.Entry name ‘classes.dex‘ collided

    有效的解决方案是把 release 文件夹下之前生成的 apk 删除,然后再次生成 apk。

    +

    22.flutter textformfield 失去焦点

    https://www.cnblogs.com/lude1994/p/14218014.html

    +
    Text('Exhaust Fan 1')
    + +

    23.InkWell 使用无效

    在外层嵌套个 inkwell,如果还是不行就在 inkwell 外面套一个 Material

    +
    Material(
    color: Colors.transparent,
    child: InkWell(
    onLongPress : (){
    print('fff');
    },
    child: Container(child: Text('Exhaust Fan 1'))
    ),
    ),
    + +

    24.瀑布流布局

    StaggeredGridView.countBuilder(
    padding: EdgeInsets.symmetric(horizontal: 15.0, vertical: 20.0),
    shrinkWrap: true,
    physics: NeverScrollableScrollPhysics(),
    primary: true,
    //滑动方向
    scrollDirection: Axis.vertical,
    //纵轴方向被划分的个数
    crossAxisCount: 2,
    //item的数量
    itemCount: _shopList.length,
    mainAxisSpacing: 10.0,
    crossAxisSpacing: 10.0,
    staggeredTileBuilder: (index) => StaggeredTile.fit(1),
    itemBuilder: (BuildContext context, int index) => GestureDetector(
    onTap: () async{
    final _app = 'amzn://'; /// 跳转到亚马逊app
    final _url = _shopList[index].url;
    if(await canLaunch(_app)) {
    await launch(_app);
    } else {
    await canLaunch(_url) ? await launch(_url) : Chili.showToast('Could not launch amazon');
    }
    },
    child: Container(/// 商品
    decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(10.0),
    boxShadow: [
    BoxShadow(
    color: Color.fromRGBO(196, 208, 226, 0.15),
    offset: Offset(6.0, 6.0),
    blurRadius: 6.0,
    )
    ]
    ),
    child: Padding(
    padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 15.0),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.center,
    children: [
    SizedBox(
    width: 151.0,
    height: 151.0,
    /// 商品图片
    child: CachedNetworkImage(
    imageUrl: '${Global.imgPath}${_shopList[index].thumbnail}',
    fit: BoxFit.cover,
    errorWidget: (context, url, error) => Icon(Icons.error),
    )
    ),
    /// 名称
    Padding(
    padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 10.0),
    child: Text(_shopList[index].name, textAlign: TextAlign.center,style: TextStyle(color: Color(0xFF333333), fontSize: 12.0)),
    ),
    /// 价格
    Text('\$${_shopList[index].price}', style: TextStyle(color: Color(0xFFFF89B6), fontSize: 14.0, letterSpacing: -0.01))
    ],
    ),
    ),
    ),
    )
    ),
    + +

    25.解决基线对齐

      +
    1. +
    +
    Row(
    crossAxisAlignment: CrossAxisAlignment.baseline,
    textBaseline: TextBaseline.alphabetic,
    children: [
    Text('反反复复:', style: TextStyle(fontSize: 40.0),),
    SizedBox(width: 10.0,),
    Text('ff', style: TextStyle(fontSize: 40.0),)
    ],
    )
    + +
      +
    1. 给 Text 设置高度
    2. +
    +

    26.图片裁剪

    /// ----------------------------主要逻辑----------------------------
    final cropKey = GlobalKey<CropState>();
    /// 处理图片
    Future _handleImage(File originalImage) async{
    final crop = cropKey.currentState;
    final scale = crop?.scale;
    final area = crop?.area;
    late File handeledImg;
    if(area == null ) {
    Chili.showToast('Save failed');
    return;
    }
    /// 请求访问照片的权限
    bool result = await ImageCrop.requestPermissions();
    if (result) {
    try {
    handeledImg = await ImageCrop.cropImage(
    file: originalImage,
    scale: scale,
    area: area
    );
    } catch(e) {
    Chili.showToast(e.toString());
    Nav.pop();
    }
    }else {
    handeledImg = originalImage; /// 失败则获取原来的图片路径
    }
    return handeledImg.path;
    }


    /// 返回处理完图片的路径
    _imgPath = await _handleImage(File(widget.image));
    + +

    27. 选取图片

    /// ----------------------------主要逻辑----------------------------
    final ImagePicker _picker = ImagePicker();
    XFile? image; /// 定义图片类型变量
    bool result = await ShowModal.showText(
    context,
    title: 'Select your operation',
    confirm: 'Camera',
    cancel: 'Album'
    );
    if(result) {
    image = await _picker.pickImage(source: ImageSource.camera, imageQuality: 40);
    } else {
    image = await _picker.pickImage(source: ImageSource.gallery, imageQuality: 40);
    }
    /// 如果用户有选取图片则路由到截图
    if(image != null) {
    Nav.push((context) => CropImageScreen(image!.path));
    print('path: ${image.path}');
    }
    + +

    28. flutter_blue

      +
    1. flutter_blue api

      +
      flutterBlue = FlutterBlue.instance.state

      flutterBlue.state
      flutterBlue.startScan()
      flutterBlue.connectedDevices
      flutterBlue.scanResults
      flutterBlue.isScanning
      flutterBlue.stopScan()
      +
    2. +
    3. BluetoothState api

      +
      enum BluetoothState {
      unknown,
      unavailable,
      unauthorized,
      turningOn,
      on,
      turningOff,
      off
      }
      +
    4. +
    5. BluetoothDeviceState

      +
      enum BluetoothDeviceState { disconnected, connecting, connected, disconnecting }
    6. +
    +

    29.自定义底部弹框

    import 'package:flutter/material.dart';
    import 'package:chili/chili.dart';

    class ShowModal {
    static Future<bool> showText(context, {title = '', text = '', confirm: 'confirm', cancel = 'cancel'}) async{
    return showModalBottomSheet(
    context: context,
    enableDrag: false,
    isDismissible: false,
    backgroundColor: Colors.white,
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.only(
    topLeft: Radius.circular(30.0),
    topRight: Radius.circular(30.0),
    )
    ),
    builder: (BuildContext context) {
    return SingleChildScrollView(
    child: Container(
    padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 15.0),
    child: Column(
    children: [
    /// 标题
    Text(title, style: TextStyle(color: Color(0xFF333333), fontSize: 16.0)),
    /// 文本
    text.isNotEmpty ? Container(
    margin: EdgeInsets.symmetric(vertical: 15.0),
    child: Text(text, textAlign: TextAlign.center, style: TextStyle(color: Color(0xFF999999), fontSize: 12.0),
    ),
    ) : Container(),
    Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
    /// 确认
    TextButton(
    onPressed: (){
    Nav.pop(true);
    },
    style: TextButton.styleFrom(
    backgroundColor: Color(0xFF5BCD49),
    padding: EdgeInsets.zero,
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(13.5)/// 外边框
    ),
    minimumSize: Size.zero
    ),
    child: Container(
    width: 70.0,
    padding: EdgeInsets.symmetric(vertical: 5.0),
    child: Center(child: Text(confirm, style: TextStyle(color: Colors.white, fontSize: 12.0))),
    )
    ),
    /// 取消
    TextButton(
    onPressed: (){
    Nav.pop(false);
    },
    style: TextButton.styleFrom(
    padding: EdgeInsets.zero,
    backgroundColor: Colors.white,
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(13.5),
    side: BorderSide(width: 0.5, color: Color(0xFF999999)) /// 外边框
    ),
    minimumSize: Size.zero
    ),
    child: Container(
    width: 70.0,
    padding: EdgeInsets.symmetric(vertical: 5.0),
    child: Center(child: Text(cancel, style: TextStyle(color: Color(0xFF999999), fontSize: 12.0))),
    )
    ),
    ],
    )
    ],
    ),
    ),
    );
    },
    ).then((value) => value);
    }
    }



    /// 调用
    bool result = await ShowModal.showText(
    context,
    title: 'Select your operation',
    confirm: 'Camera',
    cancel: 'Album'
    );
    + +

    30.常用正则表达式

    /// 常用正则验证
    /// @author [Huangch]
    class CommonRegular {

    ///手机号
    static bool isPhone(String input) {
    RegExp mobile = new RegExp(r"1[0-9]\d{9}$");
    return mobile.hasMatch(input);
    }

    ///6~16位数字和字符组合
    static bool isLoginPassword(String input) {
    RegExp password = new RegExp(r"(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,16}$");
    return password.hasMatch(input);
    }

    ///手机号屏蔽中间4位
    static String shieldPhone(String phoneNUm) {
    return phoneNUm.replaceFirst(RegExp(r"\d{4}"), "****",3);
    }

    ///身份证
    static bool isNationalId(String input) {
    RegExp id = RegExp(r'^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$');
    return id.hasMatch(input);
    }


    }
    + +

    31.关于 ?

    slide[index]?.image != null  // ?. 如果slide[index]为null,则前半部分都为null
    + +

    32.常用的登录表单布局

    child: Form(
    child: Column(
    children: [
    TextFormField(
    decoration: InputDecoration(
    prefixIconConstraints: BoxConstraints(
    maxWidth: 15.0,
    maxHeight: 18.0
    ),
    prefixIcon: Image.asset('icon_login_phone'.webp,
    fit: BoxFit.cover),
    hintStyle: TextStyle(
    color: Color(0xFFBFBFBF), fontSize: 14.0),
    hintText: '请输入手机号/邮箱'),
    ),
    TextFormField(
    decoration: InputDecoration(
    contentPadding: EdgeInsets.only(left: 20.0),
    prefixIconConstraints: BoxConstraints(
    maxWidth: 15.0,
    maxHeight: 18.0
    ),
    prefixIcon: Image.asset(
    'icon_login_yanzhengma'.webp,
    fit: BoxFit.cover,
    ),
    border: UnderlineInputBorder(
    borderSide: BorderSide(
    color: Color(0xFFF5F6F7)
    )
    ),
    hintText: '请输入验证码'))
    ],
    ),
    )
    + +

    32.网络图片缓存组件

    CachedNetworkImage(
    width: 78.0,
    height: 78.0,
    imageUrl: 'https://gitee.com/littleJshao/figure-bed/raw/master/images/dragon-1.jpg',
    progressIndicatorBuilder:
    (context, url, downloadProgress) =>
    CupertinoActivityIndicator(),
    fit: BoxFit.cover,
    errorWidget: (context, url, error) =>
    Icon(Icons.error),
    )
    + +

    33.解决华为手机在连接 Android Studio 调试时出现异常:Error while Launching activity

    百思不得其解,查找多方原因后才发现原来的应用被我从手机上卸载了,但是 Android Studio 却发现你的应用没卸载干净,导致两个应用签名不一致,所以在安装应用的时候会报出无法找到 MainActivity 入口的异常。

    +

    在终端中输入以下命令:

    +

    adb uninstall yourPakageName

    +

    博客地址

    +

    34.CustomScrollView 之 Sliver 家族的 Widget

    CustomScrollView buildCustomScrollView() {
    return CustomScrollView(
    ///反弹效果
    physics: BouncingScrollPhysics(),
    ///Sliver 家族的 Widget
    slivers: <Widget>[
    ///复杂的标题
    buildSliverAppBar(),
    ///间距
    SliverPadding(
    padding: EdgeInsets.all(5),
    ),

    ///九宫格
    buildSliverGrid(),
    ///间距
    SliverPadding(
    padding: EdgeInsets.all(5),
    ),
    ///列表
    buildSliverFixedExtentList()
    ],
    );
    }
    + +

    35.Flutter Wrap 流式布局嵌套循环 Row 导致占据一行问题

    项目中我们在使用 Wrap 去循环数据的时候,有一些 UI 需要使用到 Row 布局来进行展示,但是众所周知的是,Row 布局会占满一行,这就导致我们的 Wrap 失效了,如何解决呢?

    +
    Wrap(
    spacing:ScreenAdapter.setWidth(20),// 主轴(水平)方向间距
    runSpacing:ScreenAdapter.setHeight(13), // 纵轴(垂直)方向间距
    // alignment: WrapAlignment.start, //沿主轴方向居中
    direction:Axis.horizontal,
    children: tagList.map<Widget>((item){
    return InkWell(
    child: Container(
    padding: EdgeInsets.symmetric(horizontal:ScreenAdapter.setWidth(20), vertical:ScreenAdapter.setHeight(10)),
    decoration: BoxDecoration(
    color:Color(0xffEEEEEE),
    borderRadius: BorderRadius.circular(20)
    ),
    child:
    RichText(
    text: TextSpan(
    style: TextStyle(fontSize: 25, color: Colors.black, fontWeight:FontWeight.w500),
    children: [
    WidgetSpan(
    child: Container(
    width: 30,
    child: AspectRatio(
    aspectRatio: 1/1,
    child: ClipRRect(
    borderRadius: BorderRadius.circular(20),
    child: Container(
    color:Colors.white,
    child:Icon(Icons.add, size: 20, color:Color(0xffDB4739))
    ),
    ),
    ),
    ),
    ),
    TextSpan(text:'${item['title']}')
    ]
    ),
    )
    ),
    onTap: (){
    Navigator.pushNamed(context, '/themeDetails', arguments: {'id':'3RDOl99mWb'});
    },
    );
    }).toList()
    )
    + +

    36.flutter 输入框 TextField 设置高度以及背景色等样式的正确姿势

    37.Row 中 使用 Text,并实现超出隐藏

    Row(
    mainAxisAlignment: MainAxisAlignment.end,
    children: [
    Flexible(
    child: Text(
    '${(value?.toString()?.isEmpty ?? true) ? '请选择' : value.toString()}',
    overflow: TextOverflow.ellipsis,
    style: TextStyle(
    fontSize: 14.0, color: Color(0xFF999999))),
    ),
    Image.asset('btn_common_right'.webp,
    width: 28.0, height: 28.0, fit: BoxFit.cover)
    ],
    )
    + +

    38.DropdownButton

    DropdownButton(
    alignment: AlignmentDirectional.centerEnd,
    items: List.generate(
    _goodsType.length,
    (index) => DropdownMenuItem(
    child: Text(_goodsType[index].name!),
    value: _goodsType[index].id,
    ),
    ),
    value: goodsCategory.id,
    isDense: true,
    style: TextStyle(
    fontSize: 14.0, color: Color(0xFF333333)),
    onChanged: (val) {
    setter(() {
    goodsCategory.id = val as int;
    });
    },
    icon: Image.asset(
    'btn_common_down'.webp,
    width: 28.0,
    height: 29.0,
    fit: BoxFit.cover,
    ),
    underline: Container(),
    )
    + +

    39.复杂列表的使用,并保持销毁,初始化

    CustomScrollView(
    slivers: [
    SliverList(
    delegate: SliverChildListDelegate([
    GoogleMapScreen(
    latitude: 2.0, longitude: 2.0, onTap: (pos) {})
    ]),
    ),
    SliverList(
    delegate: SliverChildBuilderDelegate(
    (context, index) {
    List<String> images = _list[index].images!.split(';');
    return DataCard(
    info: [
    labelText('仓库名称:', '${_list[index].name}'),
    labelText('仓库地址:', '${_list[index].address}'),
    labelText('仓库电话:', '${_list[index].contactsPhone}'),
    labelText('当前距离::', '${_list[index].distance}'),
    ],
    imageWrap: images,
    btnGroup: [
    customButton(text: '一键导航', callback: () {}),
    customButton(text: '拨打电话', callback: () {}),
    ],
    );
    },
    childCount: _list.length,
    ))
    ],
    )
    + +

    40.类似 label 标签,带有外边框

    Container(
    margin: EdgeInsets.only(right: 8.0),
    padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0),
    decoration: BoxDecoration(
    color: Color.fromRGBO(255, 159, 33, 0.08),
    border: Border.all(color: Color(0xFFFF9F21)),
    borderRadius: BorderRadius.all(Radius.circular(4.0))),
    child: Text('${text}',
    style: TextStyle(color: Color(0xFFFF9F21), fontSize: 12.0)),
    )
    + +

    41.安卓内部更新 ota_update

      void _updateVersion() async{
    if (Platform.isIOS){
    String url = '${widget.data.url}'; // 这是微信的地址,到时候换成自己的应用的地址
    if (await canLaunch(url)){
    await launch(url);
    }else {
    throw 'Could not launch $url';
    }
    }else if (Platform.isAndroid){
    String url = '${widget.data.url}';
    print('下载的链接:${widget.data.url}');
    try {
    // destinationFilename 是对下载的apk进行重命名
    OtaUpdate().execute(url, destinationFilename: 'news.apk').listen(
    (OtaEvent event) {
    print('status:${event.status},value:${event.value}');
    switch(event.status){
    case OtaStatus.DOWNLOADING:// 下载中
    setState(() {
    progress = '下载进度:${event.value}%';
    });
    break;
    case OtaStatus.INSTALLING: //安装中
    print('安装中');
    progress=null;
    setState(() {});
    break;
    case OtaStatus.PERMISSION_NOT_GRANTED_ERROR: // 权限错误
    print('更新失败,请稍后再试');
    break;
    default: // 其他问题
    print('其他问题');
    break;
    }
    },
    );
    } catch (e) {
    print('更新失败,请稍后再试');
    }
    }
    }


    ///api--版本更新检查
    ApplicationFindLatestData data;
    _applicationFindLatest(String platform) async {
    await RequestUtil.applicationFindLatest(
    NoActionIntercept(this),
    platform,
    ).then((res) {
    if(res.code==200){
    data = res.data;
    Commons.isCheckUpData = true;
    print('现在版本号:${LocalStorage.get(Commons.VERSION)} 目标:${data.name}');
    if (data.name != LocalStorage.get(Commons.VERSION)) {
    // Commons.isShowUpData = true;
    showDialog(context: context,builder: (context){
    return UpdateApp(data: data,);
    });
    }else{
    showToast('已是最新版本');
    }
    setState(() {});
    }
    }).catchError((e) {});
    }
    + +

    43.打开外部链接 url_launcher

      +
    1. android 配置
    2. +
    +
    <queries>
    <!-- If your app opens https URLs -->
    <intent>
    <action android:name="android.intent.action.VIEW" />
    <data android:scheme="https" />
    </intent>
    <!-- If your app makes calls -->
    <intent>
    <action android:name="android.intent.action.DIAL" />
    <data android:scheme="tel" />
    </intent>
    <!-- If your app emails -->
    <intent>
    <action android:name="android.intent.action.SEND" />
    <data android:mimeType="*/*" />
    </intent>
    </queries>

    + +
      +
    1. ios 配置

      +
      <key>LSApplicationQueriesSchemes</key>
      <array>
      <string>https</string>
      <string>http</string>
      </array>
      +
    2. +
    3. 使用

      +
      void _launchURL() async =>
      await canLaunch(_url) ? await launch(_url) : throw 'Could not launch $_url';
    4. +
    +

    ==使用注意:url 要加特定的前缀;例如:‘tel’+url 调用拨号界面==

    +

    44.flutter 打包的 app 闪退

    在 app 下面的 build.gradle 中:

    +
    //关闭混淆, 否则在运行release包后可能出现运行崩溃, TODO后续进行混淆配置
    minifyEnabled false //删除无用代码
    shrinkResources false //删除无用资源



    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 //删除无用代码
    shrinkResources false //删除无用资源
    }
    }
    + +

    46. ios 加载(loading)

    CupertinoActivityIndicator()
    + +

    47.flutter 查看 pdf 文件

    #A package to show Native PDF View for iOS and Android, support Open from a different resource like Path, Asset or Url and Cache it.
    flutter_cached_pdfview
    + +

    48.轮播

    CarouselSlider(
    /// 轮播
    carouselController: _controller,
    options: CarouselOptions(
    autoPlay: true,
    viewportFraction: 1,
    height: 281.0,
    onPageChanged: (index, res) {
    setState(() {
    _currBanner = index;
    });
    }
    ),
    items: bannerList
    .map((data) =>
    Builder(builder: (BuildContext context) {
    return GestureDetector(
    onTap: () {
    if(data.link!.isNotEmpty) {
    Utils.openUrl(data.link!);
    return;
    }
    Nav.push(
    (context) => BannerDetailScreen(
    title: data.title!,
    bannerId: data.id!,
    ));
    },
    ///控制图片显示的尺寸主要正对下面的CachedNetworkImage修改即可,例如图片无法撑开宽度则设置width:double.maxFinite,
    child: CachedNetworkImage(
    width: double.maxFinite,
    imageUrl:
    '${Global.baseImageUrl}${data.image}',
    progressIndicatorBuilder:
    (context, url, downloadProgress) =>
    CupertinoActivityIndicator(),
    height: 281.0,
    fit: BoxFit.cover,
    errorWidget: (context, url, error) =>
    Icon(Icons.error),
    ),
    );
    }))
    )
    + +

    49.谷歌路线图

    import 'package:flutter/material.dart';
    import 'package:chili/chili.dart';
    import 'package:flutter_polyline_points/flutter_polyline_points.dart';
    import 'package:google_maps_flutter/google_maps_flutter.dart';

    /// 查看司机位置
    class CheckDriverPosition extends StatefulWidget {
    const CheckDriverPosition({required this.destLocation, required this.originLocation, Key? key}) : super(key: key);

    final Map originLocation;
    final Map destLocation;

    @override
    State<CheckDriverPosition> createState() => _CheckDriverPositionState();
    }

    class _CheckDriverPositionState extends State<CheckDriverPosition> {

    GoogleMapController? _mapController;

    /// 标记点列表
    Map<MarkerId, Marker> markers = <MarkerId, Marker>{};

    ///初始化视野 的经纬度
    double centerLat = 0 ;
    double centerLong = 0;

    Map<PolylineId, Polyline> polylines = {};
    List<LatLng> polylineCoordinates = [];
    PolylinePoints polylinePoints = PolylinePoints();
    String googleApiKey = 'AIzaSyAEF80-c_mLIj7PxKi6XU8qlkAvvH3fbhM';

    @override
    void initState() {
    // TODO: implement initState
    super.initState();
    // /// origin marker
    _addMarker(LatLng(widget.originLocation["lat"], widget.originLocation["long"]), "origin",
    BitmapDescriptor.defaultMarker);
    /// destination marker
    _addMarker(LatLng(widget.destLocation["lat"], widget.destLocation["long"]), "destination",
    BitmapDescriptor.defaultMarker);
    _getPolyline();
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    backgroundColor: Color(0xffF9F9F9),
    appBar: AppBar(
    leading: IconButton(
    onPressed: () {
    Nav.pop();
    },
    icon: Image.asset('btn_common_left'.webp,
    width: 28.0, fit: BoxFit.cover),
    ),
    title: Text(
    '查看司机位置'.i18n,
    style: TextStyle(fontSize: 18),
    ),
    ),
    body: GoogleMap(
    zoomControlsEnabled: false,
    polylines: Set<Polyline>.of(polylines.values),
    initialCameraPosition: CameraPosition(
    zoom: 10.0,
    target: LatLng(widget.originLocation['lat'], widget.originLocation['long']),
    ),
    onMapCreated: _onMapCreated,
    myLocationEnabled: true,
    // markers: Set<Marker>.of(markers.values),
    tiltGesturesEnabled: true,
    compassEnabled: true,
    ));
    }

    void _onMapCreated(GoogleMapController controller) async {
    _mapController = controller;
    }

    _addMarker(LatLng position, String id, BitmapDescriptor descriptor) {
    MarkerId markerId = MarkerId(id);
    Marker marker =
    Marker(markerId: markerId, icon: descriptor, position: position);
    markers[markerId] = marker;
    }

    _addPolyLine() {
    PolylineId id = PolylineId("poly");
    Polyline polyline = Polyline(
    polylineId: id,
    color: Color(0xFFFF9F21),
    points: polylineCoordinates,
    width: 5,
    );
    polylines[id] = polyline;
    setState(() {});
    }

    _getPolyline() async {
    await polylinePoints
    .getRouteBetweenCoordinates(
    googleApiKey,
    PointLatLng(widget.originLocation['lat'], widget.originLocation['long']),
    PointLatLng(widget.destLocation['lat'], widget.destLocation['long']),
    wayPoints: [
    PolylineWayPoint(location: "22.802306,113.164728"),
    PolylineWayPoint(location: "22.557069, 113.429766"),
    ],
    travelMode: TravelMode.driving,
    ) .then((value) {
    if (value.points.isNotEmpty) {
    value.points.forEach((PointLatLng point) {
    polylineCoordinates.add(LatLng(point.latitude, point.longitude));
    });
    } else {}
    _addPolyLine();
    });
    }

    ///获取起点与终点之间 中间的经纬度坐标
    // void getCenterLonLat(){
    // centerLat = widget.originLocation['lat'] - widget.destLocation['lat'];
    // centerLong = widget.originLocation['long'] - widget.destLocation['long'];
    // //Math.abs()绝对值
    // if( centerLong > 0){
    // centerLong = widget.originLocation['long'] - centerLong.abs() / 2;
    // }else{
    // centerLong = widget.destLocation['long'] - centerLong.abs() / 2;
    // }
    // if( centerLat > 0){
    // centerLat = widget.originLocation['lat'] - centerLat.abs() / 2;
    // }else{
    // centerLat = widget.destLocation['lat'] - centerLat.abs() / 2;
    // }
    // }

    }

    + +

    50.下载文件的 demo(不能正常运行,相关方法只做参考)

    import 'dart:isolate';
    import 'dart:ui';
    import 'dart:async';
    import 'dart:io';
    import 'package:device_info_plus/device_info_plus.dart';
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:chili/chili.dart';
    import 'package:flutter_downloader/flutter_downloader.dart';
    import 'package:path_provider/path_provider.dart';
    import 'package:permission_handler/permission_handler.dart';
    import 'package:ting_hui_wu_liu_user_app/global.dart';

    class DownloadFileWidget extends StatefulWidget {
    const DownloadFileWidget({required this.link, Key? key}) : super(key: key);

    /// 下载链接
    final String link;

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

    class _DownloadFileWidgetState extends State<DownloadFileWidget> {

    /// 当前的任务
    late TaskInfo _task;

    /// 侦听器
    ReceivePort _port = ReceivePort();

    /// 保存路径
    late String _localPath;

    /// 绑定前台和后台之间的通信
    void _bindBackgroundIsolate() {
    bool isSuccess = IsolateNameServer.registerPortWithName(
    _port.sendPort, 'downloader_send_port');
    if (!isSuccess) {
    _unbindBackgroundIsolate();
    _bindBackgroundIsolate();
    return;
    }
    _port.listen((dynamic data) {
    // if (debug) {
    // print('UI Isolate Callback: $data');
    // }
    String? id = data[0];
    DownloadTaskStatus? status = data[1];
    int? progress = data[2];

    // if (_tasks != null && _tasks!.isNotEmpty) {
    // final task = _tasks!.firstWhere((task) => task.taskId == id);
    // setState(() {
    // task.status = status;
    // task.progress = progress;
    // });
    // }
    });
    }
    /// 检查权限
    Future<bool> _checkPermission() async {
    DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
    AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
    if (Theme.of(context).platform == TargetPlatform.android &&
    androidInfo.version.sdkInt! <= 28) {
    final status = await Permission.storage.status;
    if (status != PermissionStatus.granted) {
    final result = await Permission.storage.request();
    if (result == PermissionStatus.granted) {
    return true;
    }
    } else {
    return true;
    }
    } else {
    return true;
    }
    return false;
    }

    /// 设置保存路径
    Future<String?> _findLocalPath() async {
    var externalStorageDirPath;
    if (Platform.isAndroid) {
    final directory = await getExternalStorageDirectory();
    externalStorageDirPath = directory?.path;
    } else if (Platform.isIOS) {
    externalStorageDirPath =
    (await getApplicationDocumentsDirectory()).absolute.path;
    }
    // print('externalStorageDirPath:\n$externalStorageDirPath');
    return externalStorageDirPath;
    }

    Future<void> _prepareSaveDir() async {
    _localPath = (await _findLocalPath())!;
    final savedDir = Directory(_localPath);
    bool hasExisted = await savedDir.exists();
    if (!hasExisted) {
    savedDir.create();
    }
    }

    /// 初始化存储路径,存储权限,获取本地所有任务信息
    Future<void> _prepare() async{
    /// 获取本地的所有任务信息
    final List<DownloadTask> ?tasks = await FlutterDownloader.loadTasks();
    tasks!.forEach((item) {
    if(item.url == _task.link) {
    _task.taskId = item.taskId;
    _task.status = item.status;
    _task.progress = item.progress;
    }
    });

    if(await _checkPermission()) {
    await _prepareSaveDir();
    }
    }

    /// 下载的回调
    static void downloadCallback(
    String id, DownloadTaskStatus status, int progress) {
    // if (debug) {
    // print(
    // 'Background Isolate Callback: task ($id) is in status ($status) and process ($progress)');
    // }
    final SendPort send =
    IsolateNameServer.lookupPortByName('downloader_send_port')!;
    send.send([id, status, progress]);
    }

    /// 销毁前后台之间的通信
    void _unbindBackgroundIsolate() {
    IsolateNameServer.removePortNameMapping('downloader_send_port');
    }

    /// 请求下载
    void _requestDownload(TaskInfo task) async {
    task.taskId = await FlutterDownloader.enqueue(
    url: task.link!,
    headers: {"auth": "test_for_sql_encoding"},
    savedDir: _localPath,
    showNotification: true,
    openFileFromNotification: true,
    saveInPublicStorage: true,
    );
    }

    /// 重新下载
    void _retryDownload(TaskInfo task) async {
    String? newTaskId = await FlutterDownloader.retry(taskId: task.taskId!);
    task.taskId = newTaskId;
    }

    /// 打开文件
    Future<bool> _openDownloadedFile(TaskInfo? task) {
    if (task != null) {
    return FlutterDownloader.open(taskId: task.taskId!);
    } else {
    return Future.value(false);
    }
    }

    @override
    void initState() {
    // TODO: implement initState
    super.initState();
    _task = TaskInfo(link: "${Global.baseImageUrl}${widget.link}");
    _bindBackgroundIsolate();

    FlutterDownloader.registerCallback(downloadCallback);

    _prepare();
    }

    @override
    Widget build(BuildContext context) {
    return Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
    Container(
    decoration: BoxDecoration(
    color: const Color(0xFFFF9F21),
    borderRadius: BorderRadius.circular(5.0)
    ),
    padding: const EdgeInsets.symmetric(horizontal: 42.0, vertical: 5.0),
    child: const Text('付款请求书.pdf', style: TextStyle(color: Colors.white, fontSize: 14.0),),
    ),
    TextButton(
    style: TextButton.styleFrom(
    padding: EdgeInsets.zero,
    minimumSize: Size.zero,
    tapTargetSize: MaterialTapTargetSize.shrinkWrap,
    ),
    onPressed: (){

    },
    child: _buildActionForTask(_task),
    )
    ],
    );
    }

    Widget _buildActionForTask(TaskInfo task) {
    if(task.status == DownloadTaskStatus.running) {/// 下载中
    return CircularProgressIndicator(
    value: task.progress! / 100,
    backgroundColor: Colors.white,
    valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFFF9F21)),
    );
    } else if(task.status == DownloadTaskStatus.failed) {/// 下载失败
    return IconButton(
    onPressed: () => _retryDownload(task),
    icon: Icon(Icons.refresh_rounded, size: 16.0,)
    );
    } else if(task.status == DownloadTaskStatus.complete) {
    return TextButton(
    onPressed: () => _openDownloadedFile(task),
    child: Text('打开', style: TextStyle(color: Color(0xFFFF9F21), fontSize: 14.0))
    );
    } else if (task.status == DownloadTaskStatus.enqueued) {/// 等待
    return CupertinoActivityIndicator();
    }
    return TextButton(
    onPressed: () => _retryDownload(task),
    child: Text('下载', style: TextStyle(color: Color(0xFFFF9F21), fontSize: 14.0))
    );
    }
    }

    /// 自定义任务类型数据
    class TaskInfo {
    /// 下载链接
    final String? link;

    String? taskId;
    int? progress = 0;
    DownloadTaskStatus? status = DownloadTaskStatus.undefined;

    TaskInfo({this.link});
    }
    import 'dart:isolate';
    import 'dart:ui';
    import 'dart:async';
    import 'dart:io';
    import 'package:device_info_plus/device_info_plus.dart';
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:chili/chili.dart';
    import 'package:flutter_downloader/flutter_downloader.dart';
    import 'package:path_provider/path_provider.dart';
    import 'package:permission_handler/permission_handler.dart';
    import 'package:ting_hui_wu_liu_user_app/global.dart';

    class DownloadFileWidget extends StatefulWidget {
    const DownloadFileWidget({required this.link, Key? key}) : super(key: key);

    /// 下载链接
    final String link;

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

    class _DownloadFileWidgetState extends State<DownloadFileWidget> {

    /// 当前的任务
    late TaskInfo _task;

    /// 侦听器
    ReceivePort _port = ReceivePort();

    /// 保存路径
    late String _localPath;

    /// 绑定前台和后台之间的通信
    void _bindBackgroundIsolate() {
    bool isSuccess = IsolateNameServer.registerPortWithName(
    _port.sendPort, 'downloader_send_port');
    if (!isSuccess) {
    _unbindBackgroundIsolate();
    _bindBackgroundIsolate();
    return;
    }
    _port.listen((dynamic data) {
    // if (debug) {
    // print('UI Isolate Callback: $data');
    // }
    String? id = data[0];
    DownloadTaskStatus? status = data[1];
    int? progress = data[2];

    // if (_tasks != null && _tasks!.isNotEmpty) {
    // final task = _tasks!.firstWhere((task) => task.taskId == id);
    // setState(() {
    // task.status = status;
    // task.progress = progress;
    // });
    // }
    });
    }
    /// 检查权限
    Future<bool> _checkPermission() async {
    DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
    AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
    if (Theme.of(context).platform == TargetPlatform.android &&
    androidInfo.version.sdkInt! <= 28) {
    final status = await Permission.storage.status;
    if (status != PermissionStatus.granted) {
    final result = await Permission.storage.request();
    if (result == PermissionStatus.granted) {
    return true;
    }
    } else {
    return true;
    }
    } else {
    return true;
    }
    return false;
    }

    /// 设置保存路径
    Future<String?> _findLocalPath() async {
    var externalStorageDirPath;
    if (Platform.isAndroid) {
    final directory = await getExternalStorageDirectory();
    externalStorageDirPath = directory?.path;
    } else if (Platform.isIOS) {
    externalStorageDirPath =
    (await getApplicationDocumentsDirectory()).absolute.path;
    }
    // print('externalStorageDirPath:\n$externalStorageDirPath');
    return externalStorageDirPath;
    }

    Future<void> _prepareSaveDir() async {
    _localPath = (await _findLocalPath())!;
    final savedDir = Directory(_localPath);
    bool hasExisted = await savedDir.exists();
    if (!hasExisted) {
    savedDir.create();
    }
    }

    /// 初始化存储路径,存储权限,获取本地所有任务信息
    Future<void> _prepare() async{
    /// 获取本地的所有任务信息
    final List<DownloadTask> ?tasks = await FlutterDownloader.loadTasks();
    tasks!.forEach((item) {
    if(item.url == _task.link) {
    _task.taskId = item.taskId;
    _task.status = item.status;
    _task.progress = item.progress;
    }
    });

    if(await _checkPermission()) {
    await _prepareSaveDir();
    }
    }

    /// 下载的回调
    static void downloadCallback(
    String id, DownloadTaskStatus status, int progress) {
    // if (debug) {
    // print(
    // 'Background Isolate Callback: task ($id) is in status ($status) and process ($progress)');
    // }
    final SendPort send =
    IsolateNameServer.lookupPortByName('downloader_send_port')!;
    send.send([id, status, progress]);
    }

    /// 销毁前后台之间的通信
    void _unbindBackgroundIsolate() {
    IsolateNameServer.removePortNameMapping('downloader_send_port');
    }

    /// 请求下载
    void _requestDownload(TaskInfo task) async {
    task.taskId = await FlutterDownloader.enqueue(
    url: task.link!,
    headers: {"auth": "test_for_sql_encoding"},
    savedDir: _localPath,
    showNotification: true,
    openFileFromNotification: true,
    saveInPublicStorage: true,
    );
    }

    /// 重新下载
    void _retryDownload(TaskInfo task) async {
    String? newTaskId = await FlutterDownloader.retry(taskId: task.taskId!);
    task.taskId = newTaskId;
    }

    /// 打开文件
    Future<bool> _openDownloadedFile(TaskInfo? task) {
    if (task != null) {
    return FlutterDownloader.open(taskId: task.taskId!);
    } else {
    return Future.value(false);
    }
    }

    @override
    void initState() {
    // TODO: implement initState
    super.initState();
    _task = TaskInfo(link: "${Global.baseImageUrl}${widget.link}");
    _bindBackgroundIsolate();

    FlutterDownloader.registerCallback(downloadCallback);

    _prepare();
    }

    @override
    Widget build(BuildContext context) {
    return Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
    Container(
    decoration: BoxDecoration(
    color: const Color(0xFFFF9F21),
    borderRadius: BorderRadius.circular(5.0)
    ),
    padding: const EdgeInsets.symmetric(horizontal: 42.0, vertical: 5.0),
    child: const Text('付款请求书.pdf', style: TextStyle(color: Colors.white, fontSize: 14.0),),
    ),
    TextButton(
    style: TextButton.styleFrom(
    padding: EdgeInsets.zero,
    minimumSize: Size.zero,
    tapTargetSize: MaterialTapTargetSize.shrinkWrap,
    ),
    onPressed: (){

    },
    child: _buildActionForTask(_task),
    )
    ],
    );
    }

    Widget _buildActionForTask(TaskInfo task) {
    if(task.status == DownloadTaskStatus.running) {/// 下载中
    return CircularProgressIndicator(
    value: task.progress! / 100,
    backgroundColor: Colors.white,
    valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFFF9F21)),
    );
    } else if(task.status == DownloadTaskStatus.failed) {/// 下载失败
    return IconButton(
    onPressed: () => _retryDownload(task),
    icon: Icon(Icons.refresh_rounded, size: 16.0,)
    );
    } else if(task.status == DownloadTaskStatus.complete) {
    return TextButton(
    onPressed: () => _openDownloadedFile(task),
    child: Text('打开', style: TextStyle(color: Color(0xFFFF9F21), fontSize: 14.0))
    );
    } else if (task.status == DownloadTaskStatus.enqueued) {/// 等待
    return CupertinoActivityIndicator();
    }
    return TextButton(
    onPressed: () => _retryDownload(task),
    child: Text('下载', style: TextStyle(color: Color(0xFFFF9F21), fontSize: 14.0))
    );
    }
    }

    /// 自定义任务类型数据
    class TaskInfo {
    /// 下载链接
    final String? link;

    String? taskId;
    int? progress = 0;
    DownloadTaskStatus? status = DownloadTaskStatus.undefined;

    TaskInfo({this.link});
    }
    + +

    51.WidgetSpan

    在文本中内嵌固定大小 Widget。

    +
    RichText(
    text: TextSpan(
    children: [
    WidgetSpan(
    child: Image.asset(
    '${_currIndex == index ? 'btn_login_choose_s' : 'btn_login_choose_d'}'.webp,
    width: 18.0,
    height: 18.0,
    fit: BoxFit.cover,
    ),
    ),
    WidgetSpan(
    child: Padding(
    padding: EdgeInsets.only(left: 5.0),
    child: Text('${widget.group[index]}'.i18n, style: TextStyle(fontSize: 14.0),),
    ),
    )
    ]
    ),
    )
    + +

    52.打开 google 应用的相关 api

    53.聚焦

    关于焦点事件:https://www.freesion.com/article/4272635917/

    +
    ///输入框的焦点
    FocusNode _focusNode = FocusNode();
    FocusScope.of(context).requestFocus(FocusNode());
    + +

    54.Flutter 文本输入框 TextField 属性

    57.下拉刷新,上滑加载

    import 'dart:async';
    import 'dart:io';

    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:pull_to_refresh/pull_to_refresh.dart';
    import 'package:chili/chili.dart';
    import 'package:ting_hui_wu_liu_user_app/models/model.dart';

    /// 自定义下拉刷新
    class PullDownRefresh<T> extends StatefulWidget {
    const PullDownRefresh({required this.controller, required this.future, required this.pageable, required this.builder, this.noDataText, this.noMoreDataText, Key? key}) : super(key: key);

    final Future<T> Function(Pageable pageable) future;
    final Widget Function(BuildContext context, T value) builder;
    final RefreshController controller;
    final Pageable pageable;
    /// 没有更多的数据
    final String? noMoreDataText;
    /// 没有数据
    final String? noDataText;

    @override
    _PullDownRefreshState<T> createState() => _PullDownRefreshState();
    }

    class _PullDownRefreshState<T> extends State<PullDownRefresh<T>> {
    /// 数据
    T? list;
    /// 列表是否为空
    bool isNoData = true;

    bool enableLoad = false;

    String err = '';

    @override
    void initState() {
    // TODO: implement initState
    super.initState();
    }

    void _handleError(dynamic e){
    String? message;
    widget.controller.refreshFailed();
    if (e is SocketException) {
    message = 'network.error'.i18n;
    logger.e(message, e);
    } else if (e is RpcCallException) {
    if (e.code == 401) {
    Nav.login();
    } else if(e.code == 600) {
    String language = LocalStorage.getLocale()?.languageCode ?? 'zh';
    message = e.fault?.variables[language];
    } else {
    message = e.message.i18n;
    logger.e(message, e);
    }
    } else if (e is HttpCallException) {
    message = 'http.error'.i18n;
    logger.e(message, e);
    } else if (e is TimeoutException) {
    message = 'request.timeout'.i18n;
    logger.e(message, e);
    } else {
    message = 'request.error'.i18n;
    logger.e(message, e);
    }
    if (message != null) {
    err = message;
    Chili.showToast(message);
    }
    }

    @override
    Widget build(BuildContext context) {
    return SmartRefresher(
    controller: widget.controller,
    enablePullDown: true,
    enablePullUp: enableLoad,
    header: WaterDropMaterialHeader(
    color: Color(0xFFFF9F21),
    ),
    footer: ClassicFooter(
    noDataText: widget.noMoreDataText ?? "--没有更多的数据--",
    loadingIcon: CupertinoActivityIndicator(),
    ),
    onRefresh: () {
    widget.controller.loadComplete();
    err = '';
    widget.pageable.page = 1;
    widget.future(widget.pageable).then((value) {
    list = value;
    widget.controller.refreshCompleted();
    if(mounted) {
    setState(() {
    if ((list as List).isEmpty) {
    isNoData = true;
    enableLoad = false;
    } else {
    isNoData = false;
    enableLoad = true;
    }
    });
    }
    }).catchError((e) {
    _handleError(e);
    });
    },
    onLoading: () {
    widget.pageable.page = widget.pageable.page! + 1;
    widget.future(widget.pageable).then((value) {
    if ((value as List).isEmpty) {
    widget.controller.loadNoData();
    } else {
    if(mounted) setState(() {(list as List).addAll(value);});
    widget.controller.loadComplete();
    }
    }).catchError((err) {
    _handleError(err);
    widget.controller.loadFailed();
    });
    },
    child: isNoData ? Center(
    child: Text(
    err.isNotEmpty ? err : (widget.noDataText ?? '暂无数据'),
    style: TextStyle(color: Color(0xFFBFBFBF)),
    ),
    ) : widget.builder(context, list!)
    );
    }
    }


    + +

    IntrinsicHeight

    将其子控件调整为该子控件的固有高度,举个例子来说,Row 中有 3 个子控件,其中只有一个有高度,默认情况下剩余 2 个控件将会充满父组件,而使用 IntrinsicHeight 控件,则 3 个子控件的高度一致。

    +

    文字折叠

    class TextWrapper extends StatefulWidget {
    const TextWrapper(this.text, {Key? key}) : super(key: key);

    final String text;

    @override
    State<TextWrapper> createState() => _TextWrapperState();
    }

    class _TextWrapperState extends State<TextWrapper>
    with TickerProviderStateMixin {
    bool isExpanded = false;

    @override
    Widget build(BuildContext context) {
    return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    AnimatedSize(
    vsync: this,
    duration: Duration(microseconds: 300),
    child: ConstrainedBox(
    constraints:
    isExpanded ? BoxConstraints() : BoxConstraints(maxHeight: 70),
    child: Text(
    widget.text,
    style: TextStyle(fontSize: 16),
    softWrap: true,
    overflow: TextOverflow.fade,
    ),
    ),
    ),
    isExpanded
    ? Row(
    // 使用 Row 将 btn 显示在右边,如果不使用 Row,btn 就会显示在左边
    mainAxisAlignment: MainAxisAlignment.end,
    children: [
    OutlinedButton(
    onPressed: () {
    setState(() {
    isExpanded = false;
    });
    },
    child: Text("隐藏"))
    ],
    )
    : OutlinedButton(
    onPressed: () {
    setState(() {
    isExpanded = true;
    });
    },
    child: Text("显示"))
    ],
    );
    }
    }
    + +
    class TextWrapperPage extends StatelessWidget {
    const TextWrapperPage({Key? key}) : super(key: key);

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text("显示/折叠"),
    centerTitle: true,
    backgroundColor: Colors.blue,
    ),
    body: Padding(
    padding: EdgeInsets.symmetric(vertical: 30, horizontal: 20),
    child: SingleChildScrollView(
    child: Column(
    children: [
    TextWrapper(
    "【摘要】 食品安全问题关系国计民生,一直是社会各界广泛关注的焦点。基于政策法规、主流期刊、权威媒体的三维视角,首先从\"是什么\"的角度对改革开放四十年以来我国食品安全问题关注重点的变化进行了系统梳理,总体上,我国食品安全问题关注重点的变化轨迹可描绘为\"食品数量安全→食品数量和卫生安全→食品质量安全→食品质量和营养安全\";其次进一步从\"为什么\"的角度剖析不同历史阶段我国食品安全问题关注重点变迁的内在逻辑,揭示导致以上变化的主要驱动因素;最后总结改革开放以来我国食品安全领域的重要成就,指明我国食品安全问题的发展方向。 "),
    Divider(
    height: 30,
    ),
    TextWrapper(
    "【摘要】 食品安全问题关系国计民生,一直是社会各界广泛关注的焦点。基于政策法规、主流期刊、权威媒体的三维视角,首先从\"是什么\"的角度对改革开放四十年以来我国食品安全问题关注重点的变化进行了系统梳理,总体上,我国食品安全问题关注重点的变化轨迹可描绘为\"食品数量安全→食品数量和卫生安全→食品质量安全→食品质量和营养安全\";其次进一步从\"为什么\"的角度剖析不同历史阶段我国食品安全问题关注重点变迁的内在逻辑,揭示导致以上变化的主要驱动因素;最后总结改革开放以来我国食品安全领域的重要成就,指明我国食品安全问题的发展方向。 "),
    ],
    ),
    )),
    );
    }
    }
    + +

    58.简单的单例模式写法

    class TestEventBust {
    static TestEventBust _instance = TestEventBust._init();
    /// 命名构造函数
    TestEventBust._init();
    EventBus _eventBus = EventBus();
    EventBus get bus{
    return _eventBus;
    }
    /// 工厂构造函数
    factory TestEventBust() => _instance;
    }
    + +

    50.BackdropFilter 高斯模糊/毛玻璃效果

    Flutter 自带的一个 ui 组件。

    +

    注意点:
    官方文档:The filter will be applied to all the area within its parent or ancestor widget’s clip. If there’s no clip, the filter will be applied to the full screen.

    +

    译:过滤器将应用于其父控件或祖先控件剪辑中的所有区域。如果没有剪辑,过滤器将应用于全屏。

    +
    Stack(
    fit: StackFit.expand,
    children: <Widget>[
    Text('0' * 10000),
    Center(
    child: ClipRect( // <-- clips to the 200x200 [Container] below
    child: BackdropFilter(
    filter: ui.ImageFilter.blur(
    sigmaX: 5.0,
    sigmaY: 5.0,
    ),
    child: Container(
    alignment: Alignment.center,
    width: 200.0,
    height: 200.0,
    child: const Text('Hello World'),
    ),
    ),
    ),
    ),
    ],
    )
    + +

    51.显示 SVG 格式的 Flutter 组件:flutter_svg

    ListView.builder(
    shrinkWrap: true,
    scrollDirection: Axis.vertical,
    itemCount: countries.length,
    physics: ScrollPhysics(),
    itemBuilder: (context, index){
    final Widget networkSvg = SvgPicture.network(
    '${countries[index].flag}',
    fit: BoxFit.fill,
    semanticsLabel: 'A shark?!',
    placeholderBuilder: (BuildContext context) => Container(
    padding: const EdgeInsets.all(30.0),
    child: const CircularProgressIndicator(
    backgroundColor: Colors.redAccent,
    )),);
    return
    Column(
    children: [
    ListTile(
    title: Text('${countries[index].name}'),
    leading: CircleAvatar(
    backgroundColor: Colors.white,
    child: networkSvg,
    ),
    )
    ],
    );
    });
    + +

    Flutter shape

    新了解:shapeDecoration

    +

    关于形状

    +

    通过屏幕密度选择对应尺寸的图片

    import 'dart:ui';

    import 'package:chili/chili.dart';
    import 'package:flutter/material.dart';

    import 'main_screen.dart';

    class IntroScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    double dpi = MediaQueryData.fromWindow(window).devicePixelRatio; //屏幕密度
    String prefix = '';
    final imgWidth = MediaQuery.of(context).size.width;
    if (dpi < 1.5) {
    prefix = "assets/intro/small";
    } else if (dpi < 2) {
    prefix = "assets/intro/medium";
    } else {
    prefix = "assets/intro/large";
    }
    List<Widget> pages = [
    Image.asset(
    '$prefix/1.png',
    fit: BoxFit.fill,
    ),
    Image.asset(
    '$prefix/2.png',
    fit: BoxFit.fill,
    ),
    Image.asset(
    '$prefix/3.png',
    fit: BoxFit.fill,
    ),
    Image.asset(
    '$prefix/4.png',
    fit: BoxFit.fill,
    ),
    ];
    return Introduction(
    onSkip: () {
    Nav.pushReplacement((context) => MainScreen());
    },
    pages: pages,
    next: Text('Next'),
    skip: Text('Skip'),
    );
    }
    }

    + +
    static init() {
    print("当前的屏幕密度为:$dpi");
    if (platform == 1) {
    print("当前设备为Android");
    postfix = ".png";
    if (dpi < 1) {
    basePath = Local_Icon_prefix + "android/mdpi/";
    } else if (dpi < 1.5) {
    basePath = Local_Icon_prefix + "android/hdpi/";
    } else if (dpi < 2) {
    basePath = Local_Icon_prefix + "android/xhdpi/";
    } else if (dpi < 3) {
    basePath = Local_Icon_prefix + "android/xxhdpi/";
    } else {
    basePath = Local_Icon_prefix + "android/xxxhdpi/";
    }
    } else {
    basePath = Local_Icon_prefix + "ios/";
    if (dpi < 2.5) {
    postfix = "@2x.png";
    } else {
    postfix = "@3x.png";
    }
    }
    print(basePath);
    return basePath;
    }
    +]]>
    + + Flutter + + + Flutter + +
    diff --git a/tags/Dart/index.html b/tags/Dart/index.html new file mode 100644 index 0000000..c098293 --- /dev/null +++ b/tags/Dart/index.html @@ -0,0 +1,691 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: Dart - + + Jshao's Motel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + +
    + + +
    + + + +
    + + +
    + + + +
    + +
    + + + +
    + + +
    +
    +  Dart +
    +
    + +
    + +
    +
    + 2023 + [2] +
    + +
    + +
    + +
    +
    + +
    +
    + 1 +
    + +
    + + + + +
    + + + +
    + + +
    + + + +
    +
    +
      +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + +
    • + + + + + + +
    • + +
    • +
    + +
      +
    • + +
    • + +
    • + + +
    • + + +
    +
    + +
    + +
    + +
    + + + +
    + +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + +
    + + + + diff --git a/tags/Flutter/index.html b/tags/Flutter/index.html index 2726b71..9f7148a 100644 --- a/tags/Flutter/index.html +++ b/tags/Flutter/index.html @@ -73,7 +73,7 @@ @@ -227,7 +227,7 @@
  • - PORTFOLIO + 复盘-1
  • @@ -342,7 +342,7 @@
  • - PORTFOLIO + 复盘-1
  • @@ -376,10 +376,34 @@
    2023 - [1] + [5]