Flutter

CSM

https://www.danielteng.com/courses/csm/

環境安裝

  • 官方文件

  • Aki筆記

  • 新增環境變數

    • cd $home
    • ls -al
    • nano .zshrc
    • export PATH="$PATH:/Users/${userName}/Documents/Libary/flutter/bin"
  • 透過 flutter doctor 確認環境已設定完成

  • 在當前目錄開啟終端機

  • 安裝cocoapods前要先確定Ruby的安裝版本是否一致

    • Install Ruby
      • brew install ruby
    • Install cocoapods
      • sudo gem install cocoapods

pod install 在M系列Mac上執行時,需要先執行

sudo arch -x86_64 gem install ffi
arch -x86_64 pod install
若在執行 Android Studio時出現 "Gradle project sync failed"
要去注意 Android Studio -> Tool -> SDK Manager -> SDK Platforms -> Show Package Details
版本最少安裝 Android 9(Pie) -> Android SDK Platform 28 

IDE相關設定

  • Visual Studio Code
    • Dart
    • Flutter
    • Code Spell Checker(檢查拼錯字)
    • Indent-Rainbow(縮排變色)
    • Material Icon Theme(文件圖標)
    • Prettier - Code formatter(格式化插件)

若在Visual Studio Code中,字體顏色變成 檢查主題色彩是不是Dark+,若是選擇Dark則會有上圖的情況 調整後,如下圖

套件管理工具

HomeBrew

安裝教學

M1晶片安裝完成後,需要額外執行以下語法,Homebrew才能使用

echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile

Method Channel

套件

網路行為API呼叫需要同時import 這兩個

若要做日期格式的多語系可參考 ==DateFormat(‘MM/dd EEEE’, locale).format(now);== 日期多語系

官方

參考文件

工具

Android Icon Flutter -> android -> app -> src -> main -> res -> 圖片對應路徑 iOS Icon Flutter ->iOS -> Runner -> Assets.xcassets -> 圖片對應路徑 (Contents.json => 必要檔案)

Flutter App Life Cycle

Flutter App生命週期

iOS打包發佈相關

模擬器尺寸相關

  1. 確認程式碼可以Build成功
  2. 確認環境 (下圖範例為uat) (IOS打包的時候,僅能透過Any Devices和實體機,不可用模擬器打包)
  3. 設定版號
  4. 打包
  5. 發布
  6. 依需求選擇後續設置直到 Upload 完成

Archive Manager(管理打包後的檔案) Windows -> Organizer

Android

  • Android檔案傳輸 檔案傳輸工具

  • 手機安裝APK cd Library/Android/sdk/platform-tools adb install -r 將檔案拖曳進Terminal

  • 手機截圖 adb shell screencap -p /sdcard/Download/s1.png adb pull /sdcard/Download/s1.png

  • 手機錄影 adb shell screenrecord /sdcard/Download/d1.mp4 control +c adb pull /sdcard/Download/d1.mp4

  • 線上馬賽克

  1. 確認程式碼可以Build成功
  2. 確認Version.properties 版本號
  3. 產生Apk檔
  4. 執行完畢後至Firebase將檔案上傳即可

Flutter Key的作用

Flutter Widget Key的作用

推播

FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
      if (message.notification != null) {
        Clipboard.setData(
            ClipboardData(text: "keys :" + message.data.keys.first ?? "none"));
        Future.delayed(const Duration(seconds: 3), () {});
      }
    });
    FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

檔案容量處理

getFileSize(String filepath, int decimals) async {
  var file = File(filepath);
  int bytes = await file.length();
  if (bytes <= 0) return "0 B";
  const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
  var i = (log(bytes) / log(1024)).floor();
  var gg =
      ((bytes / pow(1024, i)).toStringAsFixed(decimals)) + ' ' + suffixes[i];
  return ((bytes / pow(1024, i)).toStringAsFixed(decimals)) + ' ' + suffixes[i];
}

1 byte = 8 bits 1KB (kilobyte) = 1024b 1MB (megabyte) = 1024KB 1GB (Gigabyte) = 1024MB 1TB (trillionbyte) = 1024gb 1PB (petabyte, billions of bytes, beat bytes) = 1024tb 1eb (Exabyte) = 1024pb 1zb (zettabyte) = 1024eb 1yb (yottabyte, 100 million bytes, Yao bytes) = 1024zb 1BB (brontobyte) = 1024yb

小技巧: 
限制檔案大小上限時,不應該把檔案容量寫的與限制規格剛好,有時候檔表頭會有些META DATA 會造成太剛好的檔案上傳後檔案變大而上傳失敗!
舉例:  限制3MB檔案上傳,建議限制2.96MB

容量轉換

檔案測試來源網站


生命週期監聽

  • const & final 差異
    • Final 宣告的 property 是在專案執行(run time)階段的常數。
    • Constant 宣告的 property 是在專案編譯(compile time)階段的常數。
      final date1 = DateTime.now(); 
      const date2 = DateTime.now(); // Compile error
      

正則

參考來源

範例(最少輸入八碼,必須包含大小寫英文及數字):

  bool isMatch(String password) {
    RegExp regExpStr = RegExp(r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$');
    return regExpStr.hasMatch(password);
  }
r'^
  (?=.*[A-Z])       // should contain at least one upper case
  (?=.*[a-z])       // should contain at least one lower case
  (?=.*?[0-9])      // should contain at least one digit
  (?=.*?[!@#\$&*~]) // should contain at least one Special character
  .{8,}             // Must be at least 8 characters in length  
$

排序

Sort

上架

Fastlane自動打包發布-1

Fastlane自動打包發布-2

1.xcode-select –install 2.sudo gem install fastlane 3.到專案資料夾[IOS OR Android] 4.打開fastlane -> fasefile 看指令

  • fastlane uat
過程中遇到問題可能要更新一些東西
fastlane install_plugins
bundle exec fastlane update_plugins

憑證

IOS安裝憑證時,需要至官方網站

APPLE官方憑證

封包攔截

XCode環境

新增Config

Explicit = 只能獨一無二 (通常是域名的反解) Wildcard = 可重複使用,但是上架不能用

1.Identify 2.Profile Member ship Certificate Expire date

發布用的Certificate一個公司最多三張

ELK

在呼叫API時,寫入log到txt,在Post時,把txt內的log讀出來整理,post時在一次送出

  • 為何不在每次呼叫API就寫LOG到ELK? 如果使用者在呼叫API的過程中,手機網路異常或者有其他因素影響,會造成這個log沒有被記錄下來,而透過寫入log到txt的方式,可以透過排程補送log,查詢之前造成異常的原因,一方面也避免頻繁送log

  • 注意事項

    • 資料結構需要百分之百相同(ELK需要的格式)
    • Content-Type和Auth要設定
    • 資料在送出後,透過Flutter Console,印出來的資料前後有 "" 實際送出不會有""

Content-Type 在發送排程Log時需要使用application/vnd.api+json 但是在Flutter原生的http client在Post的時候會把Content-Type加上charset,變成 application/vnd.api+json; charset=utf-8 造成status code 回傳成功 (200 or 201) 但是Kibana沒有資料寫入 參考網址

解法:透過Dio來發送Post請求

//key = LogstashEventHubToken
//resourceUri = host 
String createToken(String resourceUri, String keyName, String key) {
  var timespan = (DateTime.now().millisecondsSinceEpoch / 1000).round();
  var week = 60 * 60 * 24 * 7;
  var expiry = (timespan + week).toString();

  String stringToSign = Uri.encodeFull(resourceUri) + "\n" + expiry;

  var keyBytes = utf8.encode(key);
  var hmacSha256 = Hmac(sha256, keyBytes); //

  var signature = base64
      .encode(hmacSha256.convert(utf8.encode(stringToSign)).bytes)
      .toString();

  var sasToken = "SharedAccessSignature sr=" +
      Uri.encodeComponent(resourceUri) +
      "&sig=" +
      Uri.encodeComponent(signature) +
      "&se=" +
      expiry +
      "&skn=" +
      keyName;
  return sasToken;
}

鍵盤

  • 收鍵盤
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
            FocusScope.of(context).unfocus();
          })
 
  • Resize頁面造成鍵盤出現時將既有元件
In your Scaffold, set **resizeToAvoidBottomPadding** property to **false**.

Android 權限設定

Flutter flow

App內顯示文章

  • WebView 隱私權條款

JavascriptMode.unrestricted 在WebView內啟用JavaScript , JavaScript預設是關閉的,若沒有啟用可能導致某些網頁顯示不出來

class PrivacyPolicyScreen extends StatefulWidget {
  const PrivacyPolicyScreen({Key? key}) : super(key: key);

  @override
  _PrivacyPolicyScreenState createState() {
    return _PrivacyPolicyScreenState();
  }
}

class _PrivacyPolicyScreenState extends State<PrivacyPolicyScreen> {

//建立Controller
  final Completer<WebViewController> _controller =
      Completer<WebViewController>();

  @override
  Widget build(BuildContext context) {
    _loadHtmlFromAssets();
    return Scaffold(
      appBar: AppBar(
        backgroundColor: xeBlue,
        elevation: 0,
        leading: (GestureDetector(
          behavior: HitTestBehavior.opaque,
          //上一頁
          child: Container(
            padding: const EdgeInsets.all(12),
            child: getImage("assets/images/new/icon_arrow_back.png",
                color: Colors.white, width: 30, height: 30, fit: BoxFit.fill),
          ),
          onTap: () {
            Navigator.pop(context);
          },
        )),
      ),
      //實際呼叫WebView
      body: WebView(
        initialUrl: 'https://flutter.dev',
        onWebViewCreated: (WebViewController webViewController) {
          _controller.complete(webViewController);
        },
      ),
    );
  }

//準備html App內顯示
  _loadHtmlFromAssets() async {
    String fileText =
        await rootBundle.loadString('assets/privacy/privacy.html');
    _controller.future.then((value) => value.loadHtmlString(fileText));
  }
}

色碼轉換

色碼轉換

GitFlow

hotfix 完成 => dev 完成 => main 發版

圖片

Image Widget

Box.fit

Android

为方法数超过 64K 的应用启用 MultiDex

multidex

iOS

佈局

Row 在自適應寬度時,要設定mainAxisSize: MainAxisSize.min不然就要設定父層寬度

 Container(
        padding: const EdgeInsets.only(left: 20, right: 20),
        child: Row(
            mainAxisSize: MainAxisSize.min,
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              CustomizeImageAsset("assets/images/new/icon_exclamation_mark.png",
                  height: 20 * heightScale, width: 20 * heightScale),
              const SizedBox(
                width: 8,
              ),
              CustomizeText(
                text: tr("PT_PleaseCheckWithinTheSetRange"),
                color: xeErrorRed,
                fontSize: 17,
                align: TextAlign.center,
              )
            ]),
      )

ListView 如果要依照放入的物件自適應高度則可以設定,如下圖放入四個語系字串。 shrinkWrap: true

鎖定轉向

螢幕轉向

await _controller.lockCaptureOrientation(); //CameraController 

演算法相關

資安

取得路徑

建立資料夾

建立資料夾

圖片閃爍

解決照片閃爍

圖片

無法打開iproxy

無法打開iproxy

Scroll(滾動、捲動)

ScrollPhysics

取得當前國家代碼

原生Flutter作法:

WidgetsBinding.instance.window.locale.countryCode
 String getCountryCode() {
    return WidgetsBinding.instance.window.locale.countryCode;
  }   //台灣取得"TW"  大陸取得"CN"

其他做法: https://stackoverflow.com/questions/57977167/device-country-in-flutter

引用 geolocator: ^5.1.1 geocoder: ^0.2.1

Future<String> getCountryName() async {
    Position position = await Geolocator().getCurrentPosition(desiredAccuracy: LocationAccuracy.high); //取得當前經緯度
    final coordinates = new Coordinates(position.latitude, position.longitude);
    var addresses = await Geocoder.local.findAddressesFromCoordinates(coordinates);
    var first = addresses.first;
    return first.countryName; // this will return country name
}

但是要注意 大陸地區無法使用這個方案,geocoder目前已沒有在維護

有出一個 geocder2 有Null safety的版本,不過需要透過Google Map在大陸地區無法使用。

單元測試

單元測試

路由管理

路由管理

自定義快捷(BottomNavigationBar)

BottomNavigationBar

新專案

  • 啟用新專案時,會有大量註解,可以透過 Ctrl+F 搜尋 //.* 用空字串替換掉,如下圖

BLOC

hydrated-bloc

Cubi & Bloc

async gives you a Future
async* gives you a Stream

模擬器&鍵盤

  • iOS Simulator 鍵盤如果預設被關閉,在做Flutter TextField 時,會變成這樣如下圖

  • 可以透過Command+K 啟用鍵盤

  • 啟用結果如下圖


  • 若鍵盤被遮蔽如下圖,可以透過 isScrollControlled來控制,如下圖

  • 可以透過flutter isScrollControlled:true 避免鍵盤被遮蔽如下圖