Flutter google_sign_in 升級到 7.x


#flutter#google_sign_in

為什麼要升級?


  1. 6.x 基於舊的 Google Sign-In SDK,已經被官方標註 deprecated
  2. 7.x 支援新的 OpenID Connect 流程,統一各平台 API
  3. 新增支援 nonce 參數,更安全的 ID token 驗證

差異


功能 6.x 7.x
GoogleSignIn 建構 GoogleSignIn(scopes: ['email']) GoogleSignIn.instance.initialize(serverClientId: ...)
signIn await _googleSignIn.signIn() await GoogleSignIn.instance.authenticate(scopeHint: ['email'], nonce: ...)
silent signIn signInSilently() attemptLightweightAuthentication()
access token 直接在 authentication 透過 authorizationClient.authorizationForScopes()

修改程式碼


6.x 寫法


try {
  final GoogleSignIn _googleSignIn = GoogleSignIn(scopes: ['email']);

  final GoogleSignInAccount? account = await _googleSignIn.signIn();
  if (account != null) {
    return GoogleUserDataModel(
      account.id,
      (await account.authentication).idToken ?? '',
      (await account.authentication).accessToken ?? '',
      account.displayName ?? '',
      account.email,
      account.photoUrl,
    );
  }
} catch (error) {
  return null;
}
return null;

7.x 寫法


try {
  await GoogleSignIn.instance.initialize(
    serverClientId: '<google-services.json → oauth_client → client_type: 3 → client_id>',
    nonce: '<optional, provide your own nonce value if required>',
  );

  GoogleSignInAccount account = await GoogleSignIn.instance.authenticate(scopeHint: ['email']);

  GoogleSignInClientAuthorization? tokenInfo = await account.authorizationClient.authorizationForScopes(['email']);

  return GoogleUserDataModel(
    account.id,
    account.authentication.idToken ?? '',
    tokenInfo?.accessToken ?? '',
    account.displayName ?? '',
    account.email,
    account.photoUrl,
  );
} catch (error) {
  return null;
}

遇到問題


iOS 滿順利的,只要 pod update 一下就好

pod update GoogleSignIn AppAuth

Android 就花了點時間,選完帳號無反應,出現 Exception: GoogleSignInExceptionCode.canceled

I/flutter (14536): GoogleSignInException(code GoogleSignInExceptionCode.canceled, activity is cancelled by the user., null)

出現這個 Exception 這一定是使用者取消,沒設定好也可能會出現

Some configuration errors will cause the underlying Android CredentialManager SDK to return a “canceled” error in this flow, and unfortunately the google_sign_in plugin has no way to distinguish this case from the user canceling sign-in, so cannot return a more accurate error message.

有幾種可能:

  • Firebase SHA-1 指紋設定錯誤,Debug 跟 Release 的都要設定。
  • serverClientId 設定錯誤,要拿 google-services.json 檔案中 oauth_clientclient_type 為 3 的 client_id
  • Android package name 錯誤。

Your google-services.json contains a web OAuth client, which should be an oauth_client entry with client_type: 3. This should have been created automatically when enabling Google Sign In using the Firebase console, but if not (or if it was later removed), add a web app to the project and then re-download google-services.json.


Flutter Markdown LaTeX Rendering


#Flutter#Dart#markdown_widget#LaTeX#Markdown

在 Flutter 中使用 markdown_widget 與 flutter_math_fork,讓 Markdown 支援 LaTeX 數學公式渲染。

Preview



Install


dependencies:
    markdown_widget: ^2.3.2+8
    flutter_math_fork: ^0.7.0

latex.dart


import 'package:flutter/material.dart';
import 'package:markdown_widget/markdown_widget.dart';
import 'package:flutter_math_fork/flutter_math.dart';
import 'package:markdown/markdown.dart' as m;

SpanNodeGeneratorWithTag latexGenerator =
    SpanNodeGeneratorWithTag(tag: _latexTag, generator: (e, config, visitor) => LatexNode(e.attributes, e.textContent, config));

const _latexTag = 'latex';

class LatexSyntax extends m.InlineSyntax {
  LatexSyntax() : super(r'(\$\$[\s\S]+\$\$)|(\$.+?\$)');

  @override
  bool onMatch(m.InlineParser parser, Match match) {
    final input = match.input;
    final matchValue = input.substring(match.start, match.end);
    String content = '';
    bool isInline = true;
    const blockSyntax = '\$\$';
    const inlineSyntax = '\$';
    if (matchValue.startsWith(blockSyntax) && matchValue.endsWith(blockSyntax) && (matchValue != blockSyntax)) {
      content = matchValue.substring(2, matchValue.length - 2);
      isInline = false;
    } else if (matchValue.startsWith(inlineSyntax) && matchValue.endsWith(inlineSyntax) && matchValue != inlineSyntax) {
      content = matchValue.substring(1, matchValue.length - 1);
    }
    m.Element el = m.Element.text(_latexTag, matchValue);
    el.attributes['content'] = content;
    el.attributes['isInline'] = '$isInline';
    parser.addNode(el);
    return true;
  }
}

class LatexNode extends SpanNode {
  final Map<String, String> attributes;
  final String textContent;
  final MarkdownConfig config;

  LatexNode(this.attributes, this.textContent, this.config);

  @override
  InlineSpan build() {
    final content = attributes['content'] ?? '';
    final isInline = attributes['isInline'] == 'true';
    final style = parentStyle ?? config.p.textStyle;
    if (content.isEmpty) return TextSpan(style: style, text: textContent);
    final latex = Math.tex(
      content,
      mathStyle: MathStyle.text,
      textStyle: style.copyWith(color: Colors.black),
      textScaleFactor: 1,
      onErrorFallback: (error) {
        return Text(textContent, style: style.copyWith(color: Colors.red));
      },
    );

    return WidgetSpan(
      alignment: PlaceholderAlignment.middle,
      child: !isInline
          ? Container(
              width: double.infinity,
              margin: EdgeInsets.symmetric(vertical: 16),
              child: Center(child: latex),
            )
          : latex,
    );
  }
}

Usage


import 'package:project_name/latex.dart';
import 'package:markdown_widget/markdown_widget.dart';

......

MarkdownWidget(
  data: '''\$\$f(X,n) = X_n + X_{n-1}\$\$
    
\$\$
M = 
\\begin{bmatrix}
\\frac{5}{6} & \\frac{1}{6} & 0 \\\\[0.3em]
\\frac{5}{6} & 0 & \\frac{1}{6} \\\\[0.3em]
0 & \\frac{5}{6} & \\frac{1}{6}
\\end{bmatrix}
\$\$''',
  markdownGenerator: MarkdownGenerator(
    inlineSyntaxList: [LatexSyntax()],
    generators: [latexGenerator],
  ),
),


......


#Flutter#Live Activity#Dynamic Island
即時動態(Live Activities)是 iOS 16.1 新增的功能,讓 APP 可以在鎖定畫面、通知中心或動態島顯示即時更新的資訊。使用者可以不必打開 APP 就能輕鬆查看一些即時資訊,提升便利性和互動體驗。


Flutter package


使用 https://pub.dev/packages/live_activities

需要寫一些 Swift,這篇文章主要介紹我使用時遇到的問題跟要注意的事項。


我的 Flutter 專案目錄結構


#Flutter#Directory Structure
├── assets                                # 檔案
│   ├── fonts                             #
│   ├── images                            #
│   └── svg                               #
├── core                                  # 
│   ├── bloc                              # BLoC
│   │   ├── global_bloc.dart              # 正式/測試、深色模式切換
│   │   ├── global_event.dart             # 
│   │   └── global_state.dart             # 
│   ├── helper                            #
│   │   ├── api.dart                      # Dio 設定 
│   │   ├── app_color.dart                # 常用色彩
│   │   ├── app_const.dart                # 一些常數
│   │   ├── app_icons.dart                # Icons
│   │   ├── app_images.dart               # Images
│   │   ├── app_key.dart                  # Storage Key
│   │   ├── app_text_size.dart            # 常用文字大小
│   │   ├── app_text_style.dart           # 常用文字 Style
│   │   ├── enum.dart                     # enum
│   │   ├── loading.dart                  # Loading 視窗
│   │   ├── toast.dart                    # 提示彈窗
│   │   └── vibrate.dart                  # 震動
│   ├── models                            #
│   │   ├── pagination_model.dart         # 處理 API 分頁固定的格式
│   │   ├── pagination_model.dart         # 
│   │   ├── response_model.dart           # 處理 API response 固定的格式
│   │   └── response_model.g.dart         #  
│   ├── repositories                      #
│   │   └── repository.dart               # Call API 的共用處理,Server Error、Timeout、Refresh Token...
│   ├── services                          # 
│   │   ├── shared                        # 
│   │   │   ├── info_service.dart         # 取得裝置資訊的 Class
│   │   │   └── permission_service.dart   # 處理獲取權限的 Class
│   │   └── global_service.dart           # 共用資料的 Singleton Class
│   └── validator                         # 驗證
│       ├── manager                       # 整合驗證
│       ├── single                        # 單個驗證
│       ├── manager_interfaces.dart       # 整合驗證 interfaces
│       └── single_interfaces.dart        # 單個驗證 interfaces
├── l10n                                  # 多國語系 arb 檔案
│   └── app_zh.arb                        # zh-tw
├── ui                                    # ui 都放在這裡面
│   ├── modal_pages                       # showModalBottomSheet 用的頁面,再利用各個單元分資料夾
│   │   └── dialog                        # showDialog 用的頁面,再利用各個單元分資料夾
│   ├── pages                             # 有 Scaffold,再利用各個單元分資料夾
│   └── widgets                           # 單純 Widget,再利用各個單元分資料夾
│       └── shared                        # 共用 Widget
│           └── fields                    # 客制的 Input、Button、Checkbox、Tag.....
├── app_link.dart                         # App Link/Universal Links
├── firebase.dart                         # Firebase 的東西
├── main.dart                             # 程式進入點
└── routes.dart                           # 路由


#Android Splash Screen#Android vector#SVG#Flutter

Flutter 提供 launch_background.xml 放置 Android 的 Splash Screen。
一般教學都是使用 PNG 圖片,但實際使用時發現很模糊,所以需要使用向量圖檔。
但直接將檔案改成 SVG 會出現 Error: The file name must end with .xml or .png,不支援 SVG 檔案,所以可使用 https://svg2vector.com 快速將 SVG 轉成 Android 的 vector 物件替換。

差異



    前往