Flutter应用开发笔记

Posted Tech Ranger

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter应用开发笔记相关的知识,希望对你有一定的参考价值。

1 获取Flutter SDK

1.下载安装包
2.将压缩包解压,然后把其中的 flutter 目录整个放在你想放置 Flutter SDK 的路径中

勿将 Flutter 安装在需要高权限的文件夹内,例如 C:\\Program Files\\。

2 配置环境变量

2.1 更新path环境变量

Environment Variables->User Variables->PATH->New加入 flutter\\bin 目录的完整路径
配置国内镜像,新增加环境变量

2.2 配置android Studio

File > Settings > Plugins下载Flutter和Dart插件

配置国内依赖
android/build.gradle替换如下内容

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

插件版本和gradle版本匹配
查看gradle版本
android/build.gradle

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

android/gradle\\wrapper\\gradle-wrapper.properties

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\\://services.gradle.org/distributions/gradle-6.5-all.zip

3 热重载(hot reload)

保持app运行状态,点击菜单栏的闪电进行热重载。

4 创建应用

在pubspec.yaml中保持如下设置,使用更多 Material 的特性

flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

5 视图(Views)

5.1 视图在Flutter中的对应概念

View是Android中显示在屏幕上的一切基础;
Widget大致是Flutter中的对应的View。

差异①:
widget 有着不一样的生命周期:它们是不可变的,一旦需要变化则生命周期终止。任何时候 widget 或它们的状态变化时, Flutter 框架都会创建一个新的 widget 树的实例。
Android View 只会绘制一次,除非调用 invalidate 才会重绘。

Flutter 的 widget 很轻量,部分原因在于它们的不可变性。因为它们本身既非视图,也不会直接绘制任何内容,而是 UI 及其底层创建真正视图对象的语义的描述。

5.2 更新widgets

差异②:
Android中可以直接更新View;
Flutter中Widget是不可变的,不能直接更新,需要操作Widget的状态。

StatelessWidget用于用户界面的一部分不依赖于除了对象中的配置信息以外的任何东西的场景。如Android中的ImageView,这个图标运行中不会改变,在Flutter中即StatelessWidget。

Text(
  'I like Flutter!',
  style: TextStyle(fontWeight: FontWeight.bold),
);

StatefulWidget用于用户界面的一部分需要和用户动态交互的部分场景。如根据 HTTP 请求返回的数据或者用户的交互来动态地更新界面,并告诉 Flutter 框架 Widget 的状态 (State) 更新了,以便 Flutter 可以更新这个 Widget。

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

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

class _SampleAppPageState extends State<SampleAppPage> {
  // Default placeholder text.
  String textToShow = 'I Like Flutter';

  void _updateText() {
    setState(() {
      // Update the text.
      textToShow = 'Flutter is Awesome!';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sample App'),
      ),
      body: Center(child: Text(textToShow)),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateText,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

这里需要着重注意的是,无状态和有状态的 Widget 本质上是行为一致的。它们每一帧都会重建,不同之处在于 StatefulWidget 有一个跨帧存储和恢复状态数据的 State 对象。
如果一个 Widget 会变化(例如由于用户交互),它是有状态的。然而,如果一个 Widget 响应变化,它的父 Widget 只要本身不响应变化,就依然是无状态的。

5.3 布局Widget

差异③:
Android中使用XML文件定义布局;
Flutter中使用Widget树定义布局。

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sample App'),
      ),
      body: Center(
        child: ElevatedButton(
          style: ElevatedButton.styleFrom(
            padding: EdgeInsets.only(left: 20.0, right: 30.0),
          ),
          onPressed: () {},
          child: Text('Hello'),
        ),
      ),
    );
  }

5.4 在布局中添加或删除组件

差异④:
Android中通过调用父 View 的 addChild() 或 removeChild() 方法动态地添加或者删除子 View;
Flutter中由于 Widget 是不可变的,所以没有 addChild() 的直接对应的方法。可以给返回一个 Widget 的父 Widget 传入一个方法,并通过布尔标记值toggle控制子 Widget 的创建。

class _SampleAppPageState extends State<SampleAppPage> {
  // Default value for toggle.
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  Widget _getToggleChild() {
    if (toggle) {
      return Text('Toggle One');
    } else {
      return ElevatedButton(
        onPressed: () {},
        child: Text('Toggle Two'),
      );
    }
  }

5.5 Widget实现动画

差异⑤:
Android中既可以通过 XML 文件定义动画,也可以调用 View 对象的 animate() 方法;
Flutter中使用动画库,通过将 Widget 嵌入一个动画 Widget 的方式实现 Widget 的动画效果。

Flutter 通过 Animation 的子类 AnimationController 来暂停、播放、停止以及逆向播放动画。它需要一个 Ticker 在垂直同步 (vsync) 的时候发出信号,并且在运行的时候创建一个介于 0 和 1 之间的线性插值。然后就可以创建一个或多个 Animation,并将它们绑定到控制器上。
下面的例子展示了如何实现一个点击 FloatingActionButton 的时候将一个 Widget 渐变为一个图标的 FadeTransition:

import 'package:flutter/material.dart';

void main() {
  runApp(FadeAppTest());
}

class FadeAppTest extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fade Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyFadeTest(title: 'Fade Demo'),
    );
  }
}

class MyFadeTest extends StatefulWidget {
  MyFadeTest({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyFadeTest createState() => _MyFadeTest();
}

class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
  AnimationController controller;
  CurvedAnimation curve;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(milliseconds: 2000),
      vsync: this,
    );
    curve = CurvedAnimation(
      parent: controller,
      curve: Curves.easeIn,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: FadeTransition(
          opacity: curve,
          child: FlutterLogo(
            size: 100.0,
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Fade',
        onPressed: () {
          controller.forward();
        },
        child: Icon(Icons.brush),
      ),
    );
  }
}

5.6 使用Canvas绘制动画

差异⑥:
Android中使用 Canvas 和 Drawable 将图片和形状绘制到屏幕上;
Flutter中使用类似于 Canvas 的 API,因为它基于相同的底层渲染引擎 Skia。

Flutter 有两个用画布 (canvas) 进行绘制的类:CustomPaint 和 CustomPainter,后者可以实现自定义的绘制算法。

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(home: DemoApp()));

class DemoApp extends StatelessWidget {
  Widget build(BuildContext context) => Scaffold(body: Signature());
}

class Signature extends StatefulWidget {
  SignatureState createState() => SignatureState();
}

class SignatureState extends State<Signature> {
  List<Offset> _points = <Offset>[];
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (DragUpdateDetails details) {
        setState(() {
          RenderBox referenceBox = context.findRenderObject();
          Offset localPosition =
              referenceBox.globalToLocal(details.globalPosition);
          _points = List.from(_points)..add(localPosition);
        });
      },
      onPanEnd: (DragEndDetails details) => _points.add(null),
      child: CustomPaint(
        painter: SignaturePainter(_points),
        size: Size.infinite,
      ),
    );
  }
}

class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);
  final List<Offset> points;
  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null)
        canvas.drawLine(points[i], points[i + 1], paint);
    }
  }

  bool shouldRepaint(SignaturePainter other) => other.points != points;
}

5.7 创建自定义Widget

差异⑦:
Android中通过继承 View 类,或者使用已有的视图类,再覆写或实现可以达到特定效果的方法;
Flutter中通过 组合 更小的 Widget 来创建自定义 Widget(而不是继承它们)。

创建一个在构造器接收标签参数的 CustomButton?你要组合 RaisedButton 和一个标签来创建自定义按钮,而不是继承 RaisedButton:

class CustomButton extends StatelessWidget {
  final String label;

  CustomButton(this.label);

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {},
      child: Text(label),
    );
  }
}

使用CustomButton:

@override
Widget build(BuildContext context) {
  return Center(
    child: CustomButton("Hello"),
  );
}

5 Intents

5.1 Intent在Flutter中对应的概念

差异⑧:
Android中Intent 主要有两个使用场景:在 Activity 之前进行导航,以及组件间通信。 Flutter 却没有 intent 这样的概念,但是你依然可以通过原生集成 (插件) 来启动 intent;
Flutter没有 Activity 和 Fragment 的对应概念。在 Flutter 中需要使用 Navigator 和 Route 在同一个 Activity 内的不同界面间进行跳转。

Route 是应用内屏幕和页面的抽象,Navigator 是管理路径 route 的工具。
一个 route 对象大致对应于一个 Activity,但是它的含义是不一样的。
Navigator 可以通过对 route 进行压栈和弹栈操作实现页面的跳转。Navigator 的工作原理和栈相似,你可以将想要跳转到的 route 压栈 (push()),想要返回的时候将 route 弹栈 (pop())。

差异⑨:
Android 中,在应用的 AndroidManifest.xml 文件中声明 Activity。
Flutter 中,有多种不同的方式在页面间导航:1)定义一个 route 名字的 Map。(MaterialApp) 2)直接导航到一个 route。(WidgetApp)

创建Map示例:

void main() {
 runApp(MaterialApp(
   home: MyAppHome(), // Becomes the route named '/'.
   routes: <String, WidgetBuilder> {
     '/a': (BuildContext context) => MyPage(title: 'page A'),
     '/b': (BuildContext context) => MyPage(title: 'page B'),
     '/c': (BuildContext context) => MyPage(title: 'page C'),
   },
 ));
}

通过将 route 名压栈 (push) 到 Navigator 中来跳转到这个 route:

Navigator.of(context).pushNamed('/b');

5.2 Flutter处理从外部应用获取接收的Intent

Flutter 可以通过直接和 Android 层通信并请求分享的数据来处理接收到的 Android intent。
示例:
首先在 Android 原生层面(在我们的 Activity 中)处理分享的文本数据,然后 Flutter 再通过使用 MethodChannel 获取这个数据。
在 AndroidManifest.xml 中注册 intent 过滤器:

<activity
  android:name=".MainActivity"
  android:launchMode="singleTop"
  android:theme="@style/LaunchTheme"
  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
  android:hardwareAccelerated="true"
  android:windowSoftInputMode="adjustResize">
  <!-- ... -->
  <intent-filter>
    <action android:name="android.intent.action.SEND" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="text/plain" />
  </intent-filter>
</activity>

在 MainActivity 中处理 intent,提取出其它 intent 分享的文本并保存。当 Flutter 准备好处理的时候,它会使用一个平台通道请求数据,数据便会从原生端发送过来:

public class MainActivity extends FlutterActivity {

  private String sharedText;
  private static final String CHANNEL = "app.channel.shared.data";

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Intent intent = getIntent();
    String action = intent.getAction();
    String type = intent.getType();

    if (Intent.ACTION_SEND.equals(action) && type != null) {
      if ("text/plain".equals(type)) {
        handleSendText(intent); // Handle text being sent
      }
    }
  }

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
      GeneratedPluginRegistrant.registerWith(flutterEngine);

      new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
              .setMethodCallHandler(
                      (call, result) -> {
                          if (call.method.contentEquals("getSharedText")) {
                              result.success(sharedText);
                              sharedText = null;
                          }
                      }
              );
  }

  void handleSendText(Intent intent) {
    sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
  }
}

当 Widget 渲染的时候,从 Flutter 这端请求数据:

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

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample Shared App Handler',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

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

class _SampleAppPageState extends State<SampleAppPage> {
  static const platform = MethodChannel('app.channel.shared.data');
  String dataShared = 'No data';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: Center(child: Text(dataShared)以上是关于Flutter应用开发笔记的主要内容,如果未能解决你的问题,请参考以下文章

Flutterflutter doctor 报错Android license status unknown. Run `flutter doctor --android-licenses‘(代码片段

flutter解决 dart:html 只支持 flutter_web 其他平台编译报错 Avoid using web-only libraries outside Flutter web(代码片段

Flutter 报错 DioError [DioErrorType.DEFAULT]: Bad state: Insecure HTTP is not allowed by platform(代码片段

Flutter 布局备忘录

Flutter学习笔记·初识Dart语言

爱了!阿里大神最佳总结“Flutter进阶学习笔记”,理论与实战