flutter入门 - 基础 - 学习笔记

Fultter学习资源相关:

  • 入门必备 - Dart、Dart学习笔记
  • Flutter中文网、英文官网
  • 一本不错的入门电子书籍
  • Flutter开发必备 - Dart仓库,你需要的这里都有
  • 清华大学Flutter镜像
  • flutter SDK 中examples文件夹下的各个示例项目

学习路线建议:Dart -> Flutter【直接品尝官方学习文档即可轻松入门 - 利用上述学习资源】

1、tips

  • flutter的超高性能 - 120fps,直接GPU进行渲染,做出来的界面,符合游戏的标准,谷歌甚至宣称flutter要比原生的要快。而RN最多60fps。
  • https://github.com/Solido/awesome-flutter
  • 学习知识要先有一个轮廓,然后再深耕。不能事无巨细,否则战线太长,效果不好。
  • 无论您导入哪个widget包,Dart都只会导入您的应用中使用的Widget。
  • Android和Flutter基于相同的底层渲染引擎Skia进行UI的绘制。

2、Flutter采用声明式UI布局方式

在声明式UI中,视图配置(例如Flutter的Widget)是不可变的,并且只是轻量级的“蓝图”。要更改UI,Widget会在自身上触发重建(最常见的是通过在Flutter中的StatefulWidgwt上调用setState)并构造一个新的Widget子树:

return ViewB(
color:red,
child: ViewC(....),
)

这里Flutter构建新的Widget实例,而不是在UI更改时改变旧实例。该框架使用RenderObjects管理传统UI对象的许多职责(例如维护布局的状态)。RenderObjects在帧之间保持不变,Flutter的轻量级Widgets告诉框架在状态之间改变RenderObjects,接下来Flutter框架会处理其余部分。

3、Flutter资源

虽然Android将resources和assets区别对待,但在Flutter中它们都会被作为assets处理,所有存在于res/drawable-*文件夹中的资源都放在Flutter的assets文件夹中。

在Flutter中assets可以是任意类型的文件,而不仅仅是图片。示例:

my-assets/data.json
记得在pubspec.yaml文件中声明assets:
assets:
 - my-assets/data.json

然后在代码中通过AssetBundle来访问它。

Flutter像iOS一样,遵循了一个简单的基于像素密度的格式。Image Asset可能是1.0x 2.0x 3.0x或者是其他任何倍数。这个devicePixelRation表示了一个物理像素到单个逻辑像素的比率。

Android不同像素密度的图片和Flutter的像素比率的对应关系:

ldpi          0.75x

mdpi        1.0x

hdpi         1.5x

xhdpi       2.0x

xxhdpi      3.0x

xxxhdpi    4.0x

Assets可以被放置到任意属性文件夹中 -- Flutter并没有预先定义的文件结构。我们需要在pubspec.yaml文件中声明assets的位置,然后Flutter会把它们识别出来。举例:

images/my_icon.png      //Base:1.0x image

images/2.0x/my_icon.png      //2.0x image

images/3.0x/my_icon.png      //3.0x image

接下来就可以再pubspec.paml文件中这样声明这个图片资源:

assets:
 - images/my-icon.png

现在,我们就可以借助AssetImage或其他访问组件来访问这个图片资源了:

return AssetImage("images/my_icon.png")

4、Flutter字符串管理

Flutter目前没有专门的字符串系统。目前,最佳做法是将strings资源作为静态字段保存在类中。例如:

class Strings {
static String msg  = "flutter";
}

默认情况下,Flutter只支持美式英语字符串。如果你要支持其他语言,请引入flutter_localizations包。你可能也要引入intl包来支持其他的i10n机制,比如日期/时间格式化。

5、关于依赖。

Flutter使用Dart构建系统和Pub包管理器来处理依赖。这些工具将Android native包装应用程序的构建委派给相应的构建系统。

在Flutter中,虽然在Flutter项目中的Android文件夹下有gradle文件,但只有在添加平添相关所需的依赖关系时才使用这些文件。否则,应该使用pubspec.yaml来声明用于Flutter的外部依赖项。

6、Widget与Android中的View区别?

Flutter布局机制的核心就是widget。在Flutter中,几乎所有东西都是一个widget - 甚至布局模型都是widget。您在Flutter应用中看到的图像、图标和文本都是widget。 甚至你看不到的东西也是widget,例如行(row)、列(column)以及用来排列、约束和对齐这些可见widget的网格(grid)。

在Flutter中,一切皆Widget。Widget本身不是视图,但是视图确实通过Widget创建的,可以简单的理解Widget为RN中的虚拟DOM。通过轻量级的Widget的执行类似DOM DIFF的算法来更新UI。

首先,Widget具有不同的生命周期:它们是不可变的,它们存在于状态被改变之前。每当Widget或其状态发生变化时,Flutter的框架都会创建一个新的Widget实例树。相比之下,Android视图被绘制一次,并且在调用invalidate之前不会重绘。

此外,与View不同,Flutter的Widget很轻巧,部分原因在于它的不变性。因为它本身不是视图,并且不是直接绘制任何东西,而是对UI及其语义的描述。

在Flutter中,Widget是不可变的,不会直接更新的。相反,我们可以通过操纵Widget的状态来更新它们。

无状态和有状态的Widget之间的最重要区别在于StatefullWidget具有State对象,该对象存储状态数据并将其传递到树重建中,因此状态不会丢失。

请记住以下规则:如果Widget在build之外更改(例如,由于运行时用户交互),则它是有状态的。如果Widget永远不会改变,一旦构建,它就是无状态的。但是,即使Widget是有状态的,如果包含它的父窗口小部件本身不对这些更改做出反应,父Widget仍然可以是无状态的。

7、如何构建自定义Widget?

在Flutter中,推荐组合多个小的Widget来构建一个自定义的widget(而不是扩展它)。

8、状态管理

状态是在构建widget时可以同步读取的信息,或者在widget的生命周期中可能更改的信息,在flutter中如果要管理状态需要使用StatefullWidget。

在设计widget时,需要考虑以下几点:

a)确定widget应该使用StatefulWidget还是StatelessWidget;

在Flutter中,Widget是有状态的还是无状态的 - 取决于是否它们依赖于状态的变化

  • 如果用户交互或者数据改变导致widget改变,那么它就是有状态的;
  • 如果一个widget是最终的或者不可变的,那么它就是无状态的;

b)确定哪个对象管理的widget的状态

在flutter中,管理状态有三种主要方式:

  • 每个widget管理自己的状态;
  • 父widget管理widget的状态;
  • 混合搭配管理的方法

如何决定使用哪种方式,参考以下原则:

  • 如果所讨论的状态是用户数据,例如复选框的已选中或未选中状态,或滑块的位置,则状态最好由父widget管理;
  • 如果widget的状态取决于动作,例如动画,那么最好是由widget自身来管理状态;
  • 如有还是不确定谁管理状态,请让父widget管理子widget的状态;

9、路由与导航

a)Flutter中不具备Intents的概念,需要的话,可以通过Native整合来触发Intent。

Intent除了可以实现Activity的跳转,另外一个作用就是外部组件的调用,比如相机等。Flutter为了实现类似的功能需要借助第三方的插件。Flutter现有的插件。

b)在Flutter中实现页面跳转。

要在Flutter中切换屏幕,我们可以访问路由以绘制新的widget。管理屏幕有两个核心概念和类:Route和Navigator。

  • Route:应用程序的“屏幕”或“页面”抽象(可以认为是Activity)
  • Navigator:管理Route的Widget,Navigator可以通过push或者pop route实现页面的切换。

以上两种widget对应flutter中实现页面导航有两种选择:

  • 具体指定一个由路由名构成的Map
  • 直接跳转至一个路由

c)如何获取路由跳转返回的结果

在Android中有startActivityForResult来获取跳转页面后返回的结果,在Flutter中Navigator类不仅用来处理路由,还被用来获取你刚push到栈中的路由返回的结果。通过await等待路由返回的结果来达到这点。

Map coordinates = await Navigator.of(context).pushNamed('/location');

之后,在location路由中,一旦用户选择了地点,携带结果一起pop出栈。

Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});

10、Dart线程模型

在Java和OC中,如果程序发生异常且没有被捕获,那么程序将会终止,但在Dart或JavaScript中则不会,究其原因,这和它们的运行机制有关系,Java和OC都是多线程模型的编程语言,任意一个线程触发异常且没被捕获时,整个进程就退出了。但Dart和JavaScript不同,它们都是单线程模型,运行机制很相似(但有区别),下面我们通过Dart官方提供的一张图来看看dart大致运行原理:

flutter入门 - 基础 - 学习笔记_第1张图片

入口函数 main() 执行完后,消息循环机制便启动了。首先会按照先进先出的顺序逐个执行微任务队列中的任务,当所有微任务队列执行完后便开始执行事件队列中的任务,事件任务执行完毕后再去执行微任务,如此循环往复,生生不息。

在Dart中,所有的外部事件任务都在事件队列中,如IO、计时器、点击、以及绘制事件等,而微任务通常来源于Dart内部,并且微任务非常少,之所以如此,是因为微任务队列优先级高,如果微任务太多,执行时间总和就越久,事件队列任务的延迟也就越久,对于GUI应用来说最直观的表现就是比较卡,所以必须得保证微任务队列不会太长。值得注意的是,我们可以通过Future.microtask(…)方法向微任务队列插入一个任务。

在事件循环中,当某个任务发生异常并没有被捕获时,程序并不会退出,而直接导致的结果是当前任务的后续代码就不会被执行了,也就是说一个任务中的异常是不会影响其它任务执行的。

11、线程与异步代码

Dart有一个单线程执行模型,支持Isolate(一种在另外一个线程上运行Dart代码的方法),一个事件循环和异步编程。除非你创建一个Isolate,否则你的Dart代码永远是运行在主UI线程,并由event loop驱动。

Dart的单线程模型并不意味着你写的代码一定要作为阻塞操作的方式运行,从而卡住UI。相反,可以使用Dart语言提供的异步工具,例如async/await,来实现异步操作。

loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = json.decode(response.body);
  });
}

一旦await的网络请求完成,通过调用setState()来更新UI,这会触发widget子树的重建,并更新相关的数据。

由于Flutter是单线程并且跑着一个event loop,因此不必担心线程管理或生成后台线程。如果正在做I/O操作,如访问磁盘或者网络请求,可以安全地使用async/await来完成。如果需要让cpu执行繁忙地计算密集型任务,你需要使用Isolate来避免阻塞event loop。

Isolate是分离的运行线程,并且不和主线程的内存堆共享内存。这意味着你不能访问主线程中的变量,或者使用setState来更新UI。正如它们的名字一样,Isolate不能共享内存。

12、图片控件 - Image

a)在加载项目中的图片资源时,为了让Image能够根据像素密度自动适配不同分辨率的照片,请使用AssetImage指定图像,并确保widget树中的“Image”widget上方存在MaterialApp,WidgetsApp或MediaQuery窗口Widget。

b)加载本地图片:需要导入dart:io包,这样就可以使用File了。

Image.file(File('/sdcard/Download/Stack.png'))

加载相对路径的的本地图片:path_provider插件。

c)如何设置placeHolder

为了设置placeHolder需要借助FadeInImage,它能够从内存、本地资源加载placeHolder。需要安装transparent_image插件。

d)图片缓存

借助cached_network_image插件即可。使用CachedNetworkImage。

13、动画

分类:补间动画、物理动画。

在Flutter中,Animation对象本身和UI渲染没有任何关系。Animation是一个抽象类,它拥有其当前值和状态(完成或者停止)。其中比较常用的Animation类是Animation

Flutter中的Animation对象是一个在一段时间内依次生成一个区间之间值的类。其输出可以是线性的、曲线的等。根据Animation对象的控制方式,动画可以反向运行,甚至可以在中间切换方向。

  • Animation还可以生成出double之外的其他类型值,如Animation或者Animation
  • Animation对象有状态。可以通过访问其value属性获取动画的当前值。
  • Animation对象本身和UI渲染没有任何关系。

当创建一个AnimationController时,需要传递一个vsync参数,存在vsync时防止屏幕外动画消耗不必要的资源(比如动画执行的时候,app退到后台),可以将statefull对象作为vsync的值。

14、轮播图组件 - flutter-swiper、MediaQuery可以去除Padding(removePadding方法)

flutter入门 - 基础 - 学习笔记_第2张图片

15、ListView通过scrollDirection即可设置是水平还是垂直方向的滚动了;

直接设置ListView的Item的高度是无效的,推荐在ListView外面包裹一层Container,通过Container的height设置高度;

通过ExpansionTile实现可折叠的列表;

通过GridView.count实现网格布局;

下拉刷新与上拉加载功能:

  - RefreshIndicator实现下拉刷新; 

  -所有的列表都支持设置一个ScrollController,通过ScrollController可以实现上拉加载更多的功能;

16、混合开发

16.1、Flutter集成的步骤

  • 创建Flutter module - 使用命令:flutter create -t module flutter_moudle;

Flutter module项目的结构:

flutter入门 - 基础 - 学习笔记_第3张图片

因为宿主项目的存在,我们的flutter module在不加额外的配置的情况下是可以独立运行的,通过安装了Flutter与Dart插件的Android Studio打开Flutter Module项目,可以直接运行。

Flutter SDK支持Android SDK版本最低16。

集成步骤:

新建一个Android工程,在工程根目录的setting.gradle中配置如下:

setBinding(new Binding([gradle: this]))                                 // new
evaluate(new File(                                                      // new
        settingsDir.parentFile,                                                // new
        'flutter_module/.android/include_flutter.groovy'// new注意:flutter_module应该和我们的Android工程在同一个目录下,如果不再,则上述路径应该有相应的调整
))  

setBinding与evaluate允许flutter模块包括它自己在内的任何flutter插件,在setting.gradle中以类似 :flutter的方式存在。完成上述的配置就可以进行依赖的添加了。

  • 添加Flutter module依赖;

在app的build.gradle,添加依赖:

implementation project(':flutter')

疑问:我们的项目下并没有flutter这个模块,为什么要添加这个依赖呢?

这是因为我们在setting.gradle的配置会自动帮我们关联到一个flutter模块。也就是在我们的项目目录下创建一个flutter模块。如下所示:

flutter入门 - 基础 - 学习笔记_第4张图片

最后,配置Java编译版本:

compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
  • 在Java中调用Flutter;

在Java中调用Flutter模块有两种方式:

  1. 使用Flutter.createView方式;
  2. 使用FlutterFragment方式;

调用Flutter module - 数据传递

通过上述1或者2两种方式,都允许我们在加载Flutter module时传递一个字符串类型的initRoute参数,Flutter的用意是传递一个要打开的页面的路由过去。但是这个参数的作用远不止这些,我们可以传递一个json串过去,包含我们需要传递的所有参数。

函数说明:public static FlutterFragment createFragment(String initialRoute){}

initialRoute:可以直接传递要页面路由 或者 传递数据;

a)页面路由名

void main() => runApp(MyApp(
      param: _getWidgetByRoute(window.defaultRouteName),
    ));

Widget _getWidgetByRoute(String route) {
  switch (route) {
    case "home":
      return HomePage();
    case "search":
      return SeachPage();
    case "my":
      return MyPage();
  }
}


b)传递的数据

loadFlutter.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                FragmentTransaction ft = getSupportFragmentManager()
                        .beginTransaction()
                        .replace(R.id.fl_container, Flutter.createFragment("{data:'I am from native'}"));
                ft.commit();
            }
        });
flutter 拿到数据就可以处理了。

16.2、flutter调试与发布

混合开发 - 热重载/热重启

在做Flutter开发的时候,它带有热重启/重新加载的功能,但是你可能会发现,混合开发中在Android项目中集成了Flutter项目,Flutter的热重启/重新加载功能好像失效了。如何启用混合开发Flutter的热重启/重新加载功能呢?

关闭我们的app,然后在Flutter module根目录运行 flutter attach命令:

flutter attach [-d 设备id]

D:\work\flutter_module>flutter attach
Waiting for a connection from Flutter on ZTE BV0720... 

然后通过AS运行app或者在手机上直接点击打开app,在app打开之后,运行我们的flutter代码,在fluuter module的terminal会看到如下的输出:

?  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".

完成上述的步骤就可以利用快捷键进行热重启/热重载了。

混合开发 - 调试

单独的flutter项目调试很方便,直接通过如下图所示的方式即可进行调试:

但是我们通过原生的项目集成flutter的方式,这个调试按钮就不好使了,为什么呢?

因为这个时候我们点击调试按钮,运行的是我们flutter module下的宿主项目.android或者.ios,而不是我们的Android原生项目。因为我们的flutter代码依赖我们的原生项目,所以需要另外的一种方式:

  • 关闭app(很关键)
  • 点击Flutter module项目中的,flutter attach按钮
  • 启动app

17、Flutter通信机制&Dart端讲解

通信场景:

flutter入门 - 基础 - 学习笔记_第5张图片

Flutter与Native的通信是通过Channel来完成的。消息使用Channel在客户端和主机之间传递。

 

flutter入门 - 基础 - 学习笔记_第6张图片

Channel中的数据都是以二进制的形式传递的,因此就需要编/解码。

Flutter定义了三种不同类型的Channel:

  • BasicMessageChannel

用于传递字符串和半结构化的信息,持续通信,收到信息后可以回复此次信息。

Dart端:
const BasicMessageChannel(this.name, this.codec);
其中
-  name,Channel的名字;
-  codec,消息的编/解码器,要和Native端保持一致,有四种类型:
BinaryCodec、StringCodec、JSONMessageCodec、StandardMessageCodec。
--------------------------------------------------------------
MessageCodec,如下所示:
abstract class MessageCodec {
  /// Encodes the specified [message] in binary.
  /// Returns null if the message is null.
  ByteData encodeMessage(T message);

  /// Decodes the specified [message] from binary.
  /// Returns null if the message is null.
  T decodeMessage(ByteData message);
}
--------------------------------------------------------------
在创建好BasicMessageChannel后,如果要让其接收Native发来的消息,需要设置一个消息处理器:
void setMessageHandler(Future handler(T message)) {
    if (handler == null) {
      BinaryMessages.setMessageHandler(name, null);
    } else {
      BinaryMessages.setMessageHandler(name, (ByteData message) async {
        return codec.encodeMessage(await handler(codec.decodeMessage(message)));
      });
    }
  }
设置之后,Dart端才能正确接收Native端传来的消息。

BasicMessageChannel是全双工通信,因此既可以接收消息也可以用于发送消息:
 Future send(T message) async {
    return codec.decodeMessage(await BinaryMessages.send(name, codec.encodeMessage(message)));
  }

Android端:
    public BasicMessageChannel(BinaryMessenger messenger, String name, MessageCodec codec) {}
- messenger
消息信使,是消息发送与接收的工具;在Android端,FlutterView就是一个BinaryMessenger; 
- name
Channel的名字,也是唯一标识符;
- codec
消息的编解码器 - 四种类型;
BinaryCodec、StringCodec、JSONMessageCodec、StandardMessageCodec。

接收Dart端发送过来的消息:
 public void setMessageHandler(BasicMessageChannel.MessageHandler handler) {}

MessageHandler的定义如下:
public interface MessageHandler {
        void onMessage(T var1, BasicMessageChannel.Reply var2);
    }

向Dart端发送消息:
public void send(T message) {    }

public void send(T message, BasicMessageChannel.Reply callback) {    }
  • MethodChannel

用于传递方法调用一次性通信。如Flutter调用Native拍照。

Dart端:
  const MethodChannel(this.name, [this.codec = const StandardMethodCodec()]);
- name:Channel的名字,要和Native端保持一致;

  Future invokeMethod(String method, [dynamic arguments]) async {}
- method:要调用Native的方法名;

Android端:
public MethodChannel(BinaryMessenger messenger, String name){} 

public MethodChannel(BinaryMessenger messenger, String name, MethodCodec codec) {}

设置消息处理器:
public void setMethodCallHandler(@Nullable MethodChannel.MethodCallHandler handler) {}

-  public interface MethodCallHandler {
        void onMethodCall(MethodCall var1, MethodChannel.Result var2);
    }
其中call是消息内容:method - 方法名;arguments:方法参数,
Result是回复此消息的回调函数给Dart端
public interface Result {
        void success(@Nullable Object var1);

        void error(String var1, @Nullable String var2, @Nullable Object var3);

        void notImplemented();
    }
即通过call.method区分调用的是哪个方法,然后再通过Result将相应的结果回传给Dart端。
  • EventChannel

.用于数据流的通信,持续通信,收到信息后无法回复此次信息,通常用于Native向Dart的通信。

Dart端:
  const EventChannel(this.name, [this.codec = const StandardMethodCodec()]);
  
提供的方法:
Stream receiveBroadcastStream([dynamic arguments]) {}

Android端:
 public EventChannel(BinaryMessenger messenger, String name) {}
 public EventChannel(BinaryMessenger messenger, String name, MethodCodec codec)

接收来自Dart端的消息:
-  public void setStreamHandler(EventChannel.StreamHandler handler){}

-  public interface StreamHandler {
        void onListen(Object var1, EventChannel.EventSink var2);

        void onCancel(Object var1);
    }
EventSink 是Native回调Dart时的会回调函数,eventSink提供了success、error、endOfStream三个回调方法分别对应事件的不同状态;

注:这三种类型的Channel都是全双工通信,即A<=>B。Dart可以主动发送信息给Android端,并且Android接收到消息可以做出回应。同样,Android端可以主动发送消息给Dart端,dart接收到数据之后,返回给Android端。

18、为什么要用final修饰Widget的成员变量?

关于二者父类 - Widget,官方文档解释:
A widget is an immutable description of part of a user interface. Widgets can be inflated 

into elements, which manage the underlying render tree.

Widgets themselves have no mutable state (all their fields must be final).

对于何时使用StatelessWidget?原则:是否仅用于展示,不存在交互。比如业务入口就是StatelessWidget,而如果是一个搜索条,则必须是StatefulWidget了。

你可能感兴趣的:(跨平台相关,flutter)