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

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

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,如下

Container(
  alignment: Alignment.topLeft,
  child: SingleChildScrollView(),
),

3.自定义AppBar

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

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

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

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

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

1.png
1.png

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

2.png
2.png

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

3.jpg
3.jpg

2.安卓打包release配置

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

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,输入

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

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

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

05D6800C-27E4-4550-A7CD-88C691BC8202.png
05D6800C-27E4-4550-A7CD-88C691BC8202.png

添加内容

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

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

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,设置好开发者证书

4.png
4.png

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

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

11.总结

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