Flutter 學習筆記

kakarotto發表於2021-06-01

專案目錄

flutter_app 
├── android       # 安卓目錄 
├── build         # 構建目錄 
├── ios           # iOS 目錄 
├── lib           # 開發目錄(相當於 src 目錄) 
| ├── main.dart   # 入口檔案(相當於 index.js) 
├── test          # 測試目錄 
├── .gitignore    # Git 提交時,設定忽略檔案內容 
├── pubspec.lock  # 專案依賴鎖定資訊(相當於 npm 中的 package-lock.json) 
└── pubspec.yaml  # 專案依賴配置(相當於 npm 中的 package.json)
複製程式碼

入口檔案

Flutter 專案的入口檔案是 lib/main.dart, 該檔案中有一個入口方法。

入口方法

// 入口方法 
void main() { 
	// 具體內容 
}
複製程式碼

根函式

void main() {
	runApp(
		// 具體內容
	);
}
複製程式碼

runApp 函式接收一個元件,並使其成為元件樹的根,框架會強制根元件覆蓋整個螢幕。

UI庫 Material

import 'package:flutter/material.dart';
複製程式碼

Mater 是一種標準的移動端和 Web 端的 UI 框架,是一套 Google 的設計規範,Flutter 專案以 Material 為 UI 基礎。

Widget(元件)

Flutter 中的一切內容都是元件,在 Flutter 當作元件一般分為以下兩類。

  • StatelessWidget

    無狀態元件,狀態不可改變的 Widget

  • StatefulWidget

    有狀態元件,持有的狀態,可能在 Widget 生命週期改變,如果我們想改變頁面中的資料,就需要用到 StatefulWidget 。

自定義元件

為了增加程式碼的可讀性,我們可以將部分程式碼分離出去,寫成獨立的 Widget。我們自定義的 Widget 需要繼承 Flutter 提供的元件,繼承的元件中有一個 build 方法,需要將我們實現的程式碼放到 build 方法中。

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text(
          'Hello Flutter',
          textDirection: TextDirection.ltr,
        ),
      ),
    );
  }
}
複製程式碼

MaterialApp

欄位型別
navigatorKey(導航主鍵)GlobalKey
home(起始頁)Widget
routes(路由列表)Map<String, WidgetBuilder>
initialRoute(初始路由名稱)String
onGenerateRoute(生成路由)RouteFactory
onUnknownRoute(未知路由)RouteFactory
navigatorObservers(導航觀察器)List
builder(構造器)TransitionBuilder
title(應用標題)String
onGenerateTitle(生成應用標題)GenerateAppTitle
color(顏色)Color
theme(主題配置)ThemeData
locale(本地化)Locale
localizationsDelegates(本地化委託代理)Iterable
localeResolutionCallback(本地化分辨回撥)LocaleResolutionCallback
supportedLocales(應用支援區域)Iterable
debugShowMaterialGrid(是否顯示 Material 網格)bool
showPerformanceOverlay(顯示效能監控疊層)bool
checkerboardRasterCacheImages(棋盤格光柵快取影像)bool
checkerboardOffscreenLayers(棋盤格層)bool
showSemanticsDebugger(顯示語義偵錯程式)bool
debugShowCheckedModeBanner(是否顯示 DEBUG 橫幅)bool

Scaffold

Scaffold 是 Flutter 應用的腳手架,用來搭建 Flutter 專案的基本佈局結構。

  • appBar

    顯示在介面頂部的一個 Appbar,也就是 Android 中的 ActionBar、Toolbar

  • body

    當前介面的主體 Widget

  • floatingActionButton

    紙墨設計中所定義的 FAB,介面的主要功能按鈕

  • ......

App 結構

MyApp

常用文字元件

  • Container
    • child(宣告子元件)
    • padding/margin
      • EdgeInsets.all()
      • EdgeInsets.fromLTRB()
      • EdgeInsets.only()
    • decoration
      • BoxDecoration(邊框、圓角、漸變、陰影、背景色、背景圖片)
    • alignment
      • Alignment (內容對齊)
    • transform
      • Matrix4(平移,旋轉,縮放,斜切)
  • Column
    • Column 中的主軸方向是垂直佈局
    • mainAxisAlignment:主軸對齊方式
    • crossAxisAlignment:交叉軸對齊方式
  • Row
    • Row 中的主軸方向是水平方向,其他屬性和 Column 一致
  • Text(用來顯示文字的元件,它是最常用的元件)
    • TextDirection(文字方向)
    • TextStyle(文字樣式)
      • Colors(文字顏色)
      • FontWeight(字型粗細)
      • FontStyles(字型樣式)
    • TextAlign(文字對齊)
    • TextOverflow(文字溢位)
    • maxLines(指定顯示的行數)
  • RichText
    • 如果一段文字需要顯示不同的樣式,Text元件無法滿足我們的需求,這個時候需要使用RichText
  • TextSpan
    • 類似 html 中的 span 標籤,TextSpan 和 RichText 結合使用可以實現不同的樣式佈局。

第三方元件

dio

  • dio 是一個強大的 Dart Http 請求庫(類似axios)

    pub.dev/packages/di…

  • 使用步驟

    • 在pubsepc.yaml 中新增 dio 依賴
    • 安裝依賴 (pub get | flutter packages get | vs code 中儲存配置,自動下載)
    • 引入 import ‘package:do/dio.dart’;
    • 使用:pub.dev/documentati…

Flutter_swiper

  • Flutter 中最好的輪播元件,適配 Android 和 iOS
  • 使用步驟
    • 在 pubsepc.yaml 中新增 flutter_swiper 依賴
    • 安裝依賴 (pub get | flutter packages get | vs code 中儲存配置,自動下載)
    • 引入 import ‘package:flutter_swiper/flutter_swiper.dart’;
    • 使用

生命週期

  • initState() 元件物件插入到元素樹中時
  • didChangeDependencies() 當前狀態物件的依賴改變時
  • build() 元件渲染時
  • setState() 元件物件的內部狀態改變時候
  • didUpdateWidget() 元件配置更新時
  • deactivate() 元件物件在元素樹中暫時移除時
  • dispose() 元件物件在元素樹中永遠移除時

路由與導航

路由簡介

  • Route

    一個路由是一個螢幕或頁面的抽象

  • Navigator

    • 管理路由的元件,Navigator 可以通過路由入棧和出棧來實現頁面之間的跳轉
    • 常用屬性
      • initalRoute:初始化路由,既預設的頁面
      • onGenerateRoter:根據規則匹配動態路由
      • onUnknownRoute:未知路由,也就是404
      • routes:路由集合

匿名路由

  • Navigator

    • push(跳轉到指定元件)

      Navigator.push(
      	context,
      	MaterialPageRoute(
      		builder: (context) => Demo(),
      	),
      );
      複製程式碼
    • pop(回退)

      Navigator.pop(context);
      複製程式碼

命名路由

  • 宣告路由

    • routes 路由表(map型別)
    • initalRoute(初始路由)
    • onUnknownRoute(未知路由)
  • 跳轉到命名路由

    Navigator.pushNamed(context,'路由名稱');
    複製程式碼
  • 配置如下

    return MaterialApp(
    	title: 'Flutter Demo',
    	routes: {
        'home':(context)=>Home(),
    		'demo': (context) => Demo(),
    	},
    	// initialRoute: 'home',
    	onUnknownRoute: (RouteSettings setting) => MaterialPageRoute(
    		builder: (context) => UnknownPage(),
    	),
    	home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
    複製程式碼

動態路由

  • 動態路由是指通過 onGenerateRoute 屬性指定的路由,它的原理是通過傳過來的路由進行動態的匹配。

    MaterialApp(
    	title: 'Flutter Demo',
    	onGenerateRoute: (RouteSettings setting) {
        print('當前路徑:' + setting.name);
        
        if (setting.name == '/') {
          return MaterialPageRoute(builder: (context) => Home());
        }
        
        if (setting.name == 'demo') {
          return MaterialPageRoute(builder: (context) => Demo());
        }
    
        var uri = Uri.parse(setting.name);
        print(uri.pathSegments);
    
        if (uri.pathSegments.length == 2 && uri.pathSegments.first == 'demo') {
          var id = uri.pathSegments[1];
          return MaterialPageRoute(builder: (context) => Demo(id: id));
        }
        return MaterialPageRoute(builder: (context) => UnknownPage());
      }
    );
    複製程式碼

螢幕適配

  • 螢幕適配

    • 螢幕尺寸五花八門,要保證一個應用在不同的終端上表現一致
  • flutter_screenutil

  • 適配原理

    • 設計稿尺寸(例如:iphone6 的尺寸是 750*1334)
    • 終端尺寸(通過 window 或 MediaQuery.of(context) 獲得)
    • 將設計稿尺寸,在終端上進行放大或縮小
  • 設計尺寸

    • designWidth:750px
    • designHeight:1334px
  • 終端尺寸(動態獲取)

    • deviceWidth:1080px
    • deviceHeight:1920px
  • 縮放比例

    • scaleWidth = deviceWidth / designWidth
    • scaleHeight = deviceHeight / designHeight
  • 安裝

    pub.dev/packages/fl…

  • 初始化設計尺寸

    ScreenUtilInit(designSize: Size(375, 667),...);
    複製程式碼
  • 設定適配尺寸

    • Flutter 1.2 之前
      • width: ScreenUtil().setWidth(100);
      • Height:ScreenUtil().setHeight(100);
    • Flutter 1.2 之後
      • width: 100.w
      • Height: 100.h

混合開發

嵌入原生 View

  1. Runner 目錄下建立 iOS View,此 View 繼承 FlutterPlatformView ,返回一個簡單的 UILabel

    //
    //  MyFlutterView.swift
    //  Runner
    //
    //  Created by 悟空 on 2021/5/31.
    //
    
    import Foundation
    import Flutter
    
    class MyFlutterView: NSObject,FlutterPlatformView {
        
        let label = UILabel()
        
        init(_ frame: CGRect,viewID: Int64,args :Any?,messenger :FlutterBinaryMessenger) {
            super.init()
            if(args is NSDictionary){
                let dict = args as! NSDictionary
                label.text  = dict.value(forKey: "text") as! String
            }
        }
        
        func view() -> UIView {
            return label
        }
        
    }
    複製程式碼
  2. 建立 MyFlutterViewFactory

    //
    //  MyFlutterViewFactory.swift
    //  Runner
    //
    //  Created by 悟空 on 2021/5/31.
    //
    
    import Foundation
    import Flutter
    
    class MyFlutterViewFactory: NSObject,FlutterPlatformViewFactory {
        
        var messenger:FlutterBinaryMessenger
        
        init(messenger:FlutterBinaryMessenger) {
            self.messenger = messenger
            super.init()
        }
        
        func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView {
            return MyFlutterView(frame,viewID: viewId,args: args,messenger: messenger)
        }
        
        func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
            return FlutterStandardMessageCodec.sharedInstance()
        }
    }
    複製程式碼
  3. AppDelegate 中註冊

    import UIKit
    import Flutter
    
    @UIApplicationMain
    @objc class AppDelegate: FlutterAppDelegate {
      override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
      ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)
        
        let registrar:FlutterPluginRegistrar = self.registrar(forPlugin: "plugins.flutter.io/custom_platform_view_plugin")!
        let factory = MyFlutterViewFactory(messenger: registrar.messenger())
        registrar.register(factory, withId: "plugins.flutter.io/custom_platform_view")
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
      }
    }
    複製程式碼
  4. 在 Flutter/lib 目錄下新建 PlatformViewDemo

    import 'package:flutter/foundation.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    
    class PlatformViewDemo extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        Widget platformView() {
          if (defaultTargetPlatform == TargetPlatform.iOS) {
            return UiKitView(
              viewType: 'plugins.flutter.io/custom_platform_view',
              creationParams: {'text': '我是 IOS 原生 View'},
              creationParamsCodec: StandardMessageCodec(),
            );
          }
          return null;
        }
    
        return Scaffold(
          appBar: AppBar(),
          body: Center(
            child: platformView(),
          ),
        );
      }
    }
    複製程式碼

與原生通訊

Flutter 與 Native 端通訊一共有以下三種方式:

  • MethodChannel:Flutter 與 Native 端相互呼叫,呼叫後可以返回結果,可以 Native 端主動呼叫,也可以Flutter主動呼叫,屬於雙向通訊。此方式為最常用的方式, Native 端呼叫需要在主執行緒中執行。
  • BasicMessageChannel:用於使用指定的編解碼器對訊息進行編碼和解碼,屬於雙向通訊,可以 Native 端主動呼叫,也可以Flutter主動呼叫。
  • EventChannel:用於資料流(event streams)的通訊, Native 端主動傳送資料給 Flutter,通常用於狀態的監聽,比如網路變化、感測器資料等。

MethodChannel

Flutter 端建立 MethodChannel 通道,用於與原生端通訊:

// com.flutter.guide.MethodChannel 是 MethodChannel 的名稱,原生端要與之對應。
var channel = MethodChannel('com.flutter.guide.MethodChannel');
複製程式碼

傳送訊息:

var result = await channel.invokeMethod('sendData',{'name': 'xiazanzhang', 'age': 18})
複製程式碼
  • 第一個參數列示method,方法名稱,原生端會解析此引數。
  • 第二個參數列示引數,型別任意,多個引數通常使用Map
  • 返回 Future,原生端返回的資料。

使用如下:

  1. Runner 目錄下建立 MethodChannelDemo 類,內容如下

    //
    //  MethodChannelDemo.swift
    //  Runner
    //
    //  Created by 悟空 on 2021/5/31.
    //
    
    import Flutter
    import UIKit
    
    public class MethodChannelDemo {
        var count =  0
        var channel:FlutterMethodChannel
        init(messenger: FlutterBinaryMessenger) {
            channel = FlutterMethodChannel(name: "com.flutter.guide.MethodChannel", binaryMessenger: messenger)
            channel.setMethodCallHandler { (call:FlutterMethodCall, result:@escaping FlutterResult) in
                if (call.method == "sendData") {
                    if let dict = call.arguments as? Dictionary<String, Any> {
                        let name:String = dict["name"] as? String ?? ""
                        let age:Int = dict["age"] as? Int ?? -1
                        result(["name":"hello,\(name)","age":age])
                    }
                }
            }
            startTimer()
        }
        
        func startTimer() {
            var timer = Timer.scheduledTimer(timeInterval:1, target: self, selector:#selector(self.tickDown),userInfo:nil,repeats: true)
        }
        @objc func tickDown(){
            count += 1
            var args = ["count":count]
            channel.invokeMethod("timer", arguments:args)
        }
    }
    複製程式碼
  2. 修改 App Delegate

    import UIKit
    import Flutter
    
    @UIApplicationMain
    @objc class AppDelegate: FlutterAppDelegate {
      override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
      ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)
        
        // 嵌入原生 view
        let registrar:FlutterPluginRegistrar = self.registrar(forPlugin: "plugins.flutter.io/custom_platform_view_plugin")!
        
        // Flutter 向 iOS View 傳送訊息
        let factory = MyFlutterViewFactory(messenger: registrar.messenger())
        registrar.register(factory, withId: "plugins.flutter.io/custom_platform_view")
        
        // MethodChannel:與原生通訊
        let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
        MethodChannelDemo(messenger: controller.binaryMessenger)
        
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
      }
    }
    複製程式碼
  3. Flutter 專案中新建 MethodChannelDemo

    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    
    class MethodChannelDemo extends StatefulWidget {
      @override
      _MethodChannelDemoState createState() => _MethodChannelDemoState();
    }
    
    class _MethodChannelDemoState extends State<MethodChannelDemo> {
      var channel = MethodChannel('com.flutter.guide.MethodChannel');
      var _data;
      var _count;
    
      @override
      void initState() {
        super.initState();
        channel.setMethodCallHandler((call) {
          setState(() {
            _count = call.arguments['count'];
          });
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(),
          body: Column(
            children: [
              SizedBox(
                height: 50,
              ),
              ElevatedButton(
                child: Text('傳送資料到原生'),
                onPressed: () async {
                  var result = await channel
                      .invokeMethod('sendData', {'name': 'xiazanzhang', 'age': 18});
                  var name = result['name'];
                  var age = result['age'];
                  setState(() {
                    _data = '$name,$age';
                  });
                },
              ),
              Text('原生返回資料:$_data'),
              Text('接收原生主動傳送資料:$_count'),
            ],
          ),
        );
      }
    }
    複製程式碼

BasicMessageChannel

Flutter 端建立 MethodChannel 通道,用於與原生端通訊:

// com.flutter.guide.BasicMessageChannel 是 BasicMessageChannel 的名稱,原生端要與之對應。
var channel = BasicMessageChannel('com.flutter.guide.BasicMessageChannel',StandardMessageCodec());
複製程式碼

傳送訊息:

var result = await channel.send({'name': 'xiazanzhang', 'age': 18});
複製程式碼
  • 引數型別任意,多個引數通常使用Map
  • 返回 Future,原生端返回的資料。

使用如下:

  1. Runner 目錄下建立 MethodChannelDemo 類,內容如下

    //
    //  BasicMessageChannelDemo.swift
    //  Runner
    //
    //  Created by 悟空 on 2021/5/31.
    //
    
    import Flutter
    import UIKit
    
    public class BasicMessageChannelDemo {
        
        var channel:FlutterBasicMessageChannel
        
        init(messenger: FlutterBinaryMessenger) {
            channel = FlutterBasicMessageChannel(name: "com.flutter.guide.BasicMessageChannel", binaryMessenger: messenger)
            channel.setMessageHandler { (message, reply) in
                if let dict = message as? Dictionary<String, Any> {
                    let name:String = dict["name"] as? String ?? ""
                    let age:Int = dict["age"] as? Int ?? -1
                    reply(["name":"hello,\(name)","age":age])
                }
            }
        }
    }
    複製程式碼
  2. 修改 AppDelegate 類,在 application 方法中新增如下程式碼

      // BasicMessageChannel: 與 Flutter 通訊
      BasicMessageChannelDemo(messenger: controller.binaryMessenger)
    複製程式碼
  3. Flutter 專案中新建 BasicMessageChannelDemo

    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    
    class BasicMessageChannelDemo extends StatefulWidget {
      @override
      _BasicMessageChannelDemoState createState() =>
          _BasicMessageChannelDemoState();
    }
    
    class _BasicMessageChannelDemoState extends State<BasicMessageChannelDemo> {
      var channel = BasicMessageChannel(
          'com.flutter.guide.BasicMessageChannel', StandardMessageCodec());
    
      var _data;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(),
          body: Column(
            children: [
              SizedBox(
                height: 50,
              ),
              ElevatedButton(
                child: Text('傳送資料到原生'),
                onPressed: () async {
                  var data = {'name': 'xiazanzhang', 'age': 18};
                  var result = await channel.send(data) as Map;
                  var name = result['name'];
                  var age = result['age'];
                  setState(() {
                    _data = '$name,$age';
                  });
                },
              ),
              Text('原生返回資料:$_data'),
            ],
          ),
        );
      }
    }
    複製程式碼

EventChannel

Flutter 端建立 EventChannel 通道,用於與原生端通訊:

// com.flutter.guide.EventChannel 是 EventChannel 的名稱,原生端要與之對應。
var _eventChannel = EventChannel('com.flutter.guide.EventChannel');
複製程式碼

監聽原生端傳送的訊息:

var _data;
  @override
  void initState() {
    super.initState();
    _eventChannel.receiveBroadcastStream().listen(_onData);
  }

  _onData(event){
    setState(() {
      _data = event;
    });
  }
複製程式碼

使用如下:

  1. Runner 目錄下建立 MethodChannelDemo 類,內容如下

    //
    //  EventChannelDemo.swift
    //  Runner
    //
    //  Created by 悟空 on 2021/5/31.
    //
    
    import Flutter
    import UIKit
    
    public class EventChannelDemo:NSObject, FlutterStreamHandler{
        
        var channel:FlutterEventChannel?
        var count =  0
        var events:FlutterEventSink?
        
        public override init() {
            super.init()
        }
        
        convenience init(messenger: FlutterBinaryMessenger) {
            
            self.init()
            
            channel = FlutterEventChannel(name: "com.flutter.guide.EventChannel", binaryMessenger: messenger)
            channel?.setStreamHandler(self)
            startTimer()
        }
        
        func startTimer() {
            let timer = Timer.scheduledTimer(timeInterval:1, target: self, selector:#selector(self.tickDown),userInfo:nil,repeats: true)
        }
        @objc func tickDown(){
            count += 1
            let args = ["count":count]
            if(events != nil){
                events!(args)
            }
        }
        
        public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
            self.events = events
            return nil;
        }
        
        public func onCancel(withArguments arguments: Any?) -> FlutterError? {
            self.events = nil
            return nil;
        }
    
    }
    複製程式碼
  2. 修改 AppDelegate 類,在 application 方法中新增如下程式碼

    // EventChannel: 與 Flutter 通訊
    EventChannelDemo(messenger: controller.binaryMessenger)
    複製程式碼
  3. Flutter 專案中新建 BasicMessageChannelDemo

    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    
    class EventChannelDemo extends StatefulWidget {
      @override
      _EventChannelDemoState createState() => _EventChannelDemoState();
    }
    
    class _EventChannelDemoState extends State<EventChannelDemo> {
      var _eventChannel = EventChannel('com.flutter.guide.EventChannel');
      var _data;
      @override
      void initState() {
        super.initState();
        _eventChannel.receiveBroadcastStream().listen(_onData);
      }
    
      _onData(event) {
        setState(() {
          _data = event;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(),
          body: Center(
            child: Text('監聽原生返回的資料:$_data'),
          ),
        );
      }
    }
    複製程式碼

相關文章