目录

Flutter各种踩坑心得

1.安卓开发环境的基本配置

首先修改你创建的项目中的/android/build.gradle文件,修改buildscript以及allprojects的仓库地址,否则编译打包会卡住不动,其次就是package get会卡住,原因是因为连接不上谷歌的服务,有点蛋疼

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
buildscript {
    repositories {
        // google()
        // jcenter()
        maven { url 'https://maven.aliyun.com/repository/google' }
        maven { url 'https://maven.aliyun.com/repository/jcenter' }
        maven { url 'https://maven.aliyun.com/nexus/content/groups/public' }
    }
}

allprojects {
    repositories {
        // google()
        // jcenter()
        maven { url 'https://maven.aliyun.com/repository/google' }
        maven { url 'https://maven.aliyun.com/repository/jcenter' }
        maven { url 'https://maven.aliyun.com/nexus/content/groups/public' }
    }

2.SignleChildScrollView页面不满一屏无法撑满全屏

可以换个布局组件或者将其设置为Container子组件,设置alignment,如下

1
2
3
4
Container(
  alignment: Alignment.topLeft,
  child: SingleChildScrollView(),
),

3.自定义AppBar

AppBar这个组件可拓展性还是比较低的,尤其是它的高度都是写死的,这种情况可以换SliverAppBar或者自定义AppBar,换句话说自定义组件,这里就不详诉

4.push页面返回,保持上一次的页面位置状态

使用 StatefulWidget 混入 AutomaticKeepAliveClientMixin,覆盖wantKeepAlive属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class TesPage extends StatefulWidget {
  @override
  createState() => TesPageState();
}

class TesPageState extends State<TesPage> with AutomaticKeepAliveClientMixin {
  
  @override
  bool get wantKeepAlive => true;
}

5.路由跳转方式的选择

通常路由跳转会用到Navigator.push推入路由栈,Navigator.pop就移出、返回上一个的状态。如果在页面上全部用这两个方法,你会发现你的页面间的跳转仿佛乱了一样,原因就是Navigator.push是每次将你的路由推入栈,也就是你可以一直做返回操作、手势来获取之前的页面状态。这个时候就要考虑用其他方式,如

pushAndRemoveUntil:跳转到指定路由,删除先前的路由栈 pushNamed:跳转到指定命名路由,路由栈不会删除。这种方式需要配合命名路由使用,即在入口文件MaterialApp配置好routes pushNamedAndRemoveUntil:跳转到指定路由,删除先前的路由栈 pushReplacement:路由替换 pushReplacementNamed:跳转到指定命名路由,并删除最后的路由

6.页面初始化时拿到context

有时候,可能会在页面初始化使用context,比如说路由相关的Navigator.of(context)。解决方法很简单,在initState方法中,this指向上下文,即使用this.context即可以保存页面初始的context

7.键盘弹出时将布局打乱,或者说将元素顶起来了

Scafold 里设置 resizeToAvoidBottomInset: false,键盘会遮住布局,而不是顶起布局。

8.无法设置虚线边框

说实话当时发现Flutter无法设置虚线边框的时候我是很震惊的,我感觉这算是基本的不能在基本的属性了,解决方法,参考官方issues中大佬的解法,目前只能自己实现paint方法,自己用Canvas

  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
import 'package:flutter/material.dart';
import 'package:path_drawing/path_drawing.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Container(
              decoration: BoxDecoration(
                border: DashPathBorder.all(
                  dashArray: CircularIntervalList<double>(<double>[5.0, 2.5]),
                ),
              ),
              padding: const EdgeInsets.all(20.0),
              child: const Text('You have pushed the button this many times:'),
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

class DashPathBorder extends Border {
  DashPathBorder({
    @required this.dashArray,
    BorderSide top = BorderSide.none,
    BorderSide left = BorderSide.none,
    BorderSide right = BorderSide.none,
    BorderSide bottom = BorderSide.none,
  }) : super(
          top: top,
          left: left,
          right: right,
          bottom: bottom,
        );

  factory DashPathBorder.all({
    BorderSide borderSide = const BorderSide(),
    @required CircularIntervalList<double> dashArray,
  }) {
    return DashPathBorder(
      dashArray: dashArray,
      top: borderSide,
      right: borderSide,
      left: borderSide,
      bottom: borderSide,
    );
  }
  final CircularIntervalList<double> dashArray;

  @override
  void paint(
    Canvas canvas,
    Rect rect, {
    TextDirection textDirection,
    BoxShape shape = BoxShape.rectangle,
    BorderRadius borderRadius,
  }) {
    if (isUniform) {
      switch (top.style) {
        case BorderStyle.none:
          return;
        case BorderStyle.solid:
          switch (shape) {
            case BoxShape.circle:
              assert(borderRadius == null,
                  'A borderRadius can only be given for rectangular boxes.');
              canvas.drawPath(
                dashPath(Path()..addOval(rect), dashArray: dashArray),
                top.toPaint(),
              );
              break;
            case BoxShape.rectangle:
              if (borderRadius != null) {
                final RRect rrect =
                    RRect.fromRectAndRadius(rect, borderRadius.topLeft);
                canvas.drawPath(
                  dashPath(Path()..addRRect(rrect), dashArray: dashArray),
                  top.toPaint(),
                );
                return;
              }
              canvas.drawPath(
                dashPath(Path()..addRect(rect), dashArray: dashArray),
                top.toPaint(),
              );

              break;
          }
          return;
      }
    }

    assert(borderRadius == null,
        'A borderRadius can only be given for uniform borders.');
    assert(shape == BoxShape.rectangle,
        'A border can only be drawn as a circle if it is uniform.');

    // TODO(dnfield): implement when borders are not uniform.
  }
}

9.使用Dio时,设置相应数据的媒体类型

使用Dio封装网络请求,普通的请求最好设置相应类型dio.options.responseType = ResponseType.json,否则可能接收到的响应可能是字符串。

10.build配置

1.安卓的SHA1码获取方式

很多第三方应用都需要用到安卓的SHA1码,在Flutter应用中,首先用Andriod Studio打开项目,随意选择一个文件,点击右上角Open for Editing in Android Studio

https://i.loli.net/2020/12/05/KrYUhiXpHu3cLBs.png

打开Terminal,输入gradlew signingReport,mac环境需要指定当前目录

https://i.loli.net/2020/12/05/BxEfHGOAtQcLSn7.png

最后控制台会打印出应用的各种签名信息,复制SHA1码即可

https://i.loli.net/2020/12/05/6cKCEUzuBRS5Ohw.jpg

2.安卓打包release配置

首先打开/android/build.gradle文件,在buildscript中指定kotlin_version,否则debugrelease编译会不通过

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
buildscript {
    ext.kotlin_version = '1.3.50'
    repositories {
        // google()
        // jcenter()
        maven { url 'https://maven.aliyun.com/repository/google' }
        maven { url 'https://maven.aliyun.com/repository/jcenter' }
        maven { url 'https://maven.aliyun.com/nexus/content/groups/public' }
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

然后生成key,同样打开Andriod Studio,打开Terminal,输入

1
keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key

生成过程中会让你输入存储密码和文件密码,生成完key后记住文件地址

然后在在android目录中创建一个文件,文件名为:key.properties

https://i.loli.net/2020/12/05/MuVAtUaI3TojQi6.jpg

添加内容

1
2
3
4
storePassword = // 和生成key输入的存储密码一致
keyPassword = // 和生成key输入的密码一致
keyAlias = key // 
storeFile = // key文件的存储路径

找到android/app/build.gradle文件,添加签名配置singingConfig以及打包配置buildTypes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
android {
		.....
    signingConfigs {
        release {
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
            storeFile file(keystoreProperties['storeFile'])
            storePassword keystoreProperties['storePassword']
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
            ndk {
                abiFilters 'armeabi-v7a','x86' // 指定打包平台
            }
        }
    }
}

然后进入项目根目录,执行flutter build apk,即可以打包一个安卓apk包

3.iOS调试打包注意事项

iOS模拟器调试体验比安卓好的多,但是真机调试,首先你得有个苹果ID,然后用Xcode打开项目,点击Signing & Capabilities,找到Team设置,设置一个开发账号,可以是未付费注册的苹果ID,设置好开发者证书

https://i.loli.net/2020/12/05/IM1z4KEkUxPFAvl.jpg

然后flutter run的时候,选择一个真机设备,即可在真机上调试app了,但是未付费注册的苹果ID证书有效期只有七天,七天后你手机上的app就不可用了。

未付费注册的账号也无法打包上传App Store,不过配置完了开发团队、账号后、开发证书后,可以直接flutter build ios,这样会生成一个文件。这时候创建一个名为Payload的文件夹,将打包生成的文件放进去、压缩,然后将压缩文件后缀改为ipa,即可以生成一个ipa文件了。然后借助第三方工具,如itunes(貌似已经不能用)、xxx助手即可将这个文件安装在终端上运行。

11.总结

断断续续做这个小项目已经有几个月了,因为平常很忙,所以拖了这么久。Flutter给我的感觉优点就是流程、动画较为细腻,相对于其它跨平台方案,使用JsBrigdenative端通信,有较大的优势,毕竟它是自己的引擎绘制的页面。但是它的官方组件太少,自定义组件所需精力较多(可能是我不熟悉),iOS风格组件虽然样式和原生的差不多,但具体效果还是有差距。这种跨平台框架终于还是得要和原生开发人员一起操作才玩的溜。