Tag: flutter

  • Flutter StreamBuilder & Ping 套件 Example

    Flutter StreamBuilder & Ping 套件 Example

    相較於一次性的非同步執行緒 FutureBuilder,StreamBuilder 能持續運作並可搭配 StreamController 達到更新畫面資料時,不需要整個重新 build 而只更動使用 StreamBuilder 的區塊。

    以下StreamBuilder 範例為使用 dart_ping 套件,建立一個簡易的網址偵測 App。

    程式規劃和引用的套件(pubspec.yaml)

    main.dart

    import 'package:flutter/material.dart';

    import 'package:network_toolkit/screens/ping_screen.dart';

    void main() {
    runApp(
    const AppEntryPoint()
    );
    }

    class AppEntryPoint extends StatelessWidget {
    const AppEntryPoint({super.key});

    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    title: 'Network Toolkit',
    theme: ThemeData(
    useMaterial3: true,
    primarySwatch: Colors.green,
    ),
    routes: {
    "/ping":(BuildContext context) => const PingScreen(),
    },
    initialRoute: "/ping",
    );
    }
    }

    screens/ping_screen.dart

    import 'package:flutter/material.dart';
    import 'dart:async';
    import 'package:dart_ping/dart_ping.dart';

    import 'package:network_toolkit/components/ping_result_component.dart';

    class PingScreen extends StatefulWidget {
    const PingScreen({super.key});

    @override
    State createState() {
    return _PingScreen();
    }
    }

    class _PingScreen extends State<PingScreen> {
    bool _userInputTextFieldEnabled = true;
    String _urlOrIP = "";
    late StreamController _controller;
    final ScrollController _scrollController = ScrollController();
    List<String> _pingResultList = [];

    @override
    void initState() {
    super.initState();
    _controller = StreamController.broadcast(); // Initial
    }

    @override
    Widget build(BuildContext context) {
    var textEditController = TextEditingController( text: _urlOrIP );

    Widget userInputTextField = SizedBox(
    width: 500,
    child: TextField(
    decoration: const InputDecoration(
    border: OutlineInputBorder(),
    hintText: 'Input URL or IP',
    ),
    controller: textEditController,
    enabled: _userInputTextFieldEnabled,
    onSubmitted: (inputvalue){
    _urlOrIP = inputvalue;
    _pingResultList = [];

    setState(() {
    _userInputTextFieldEnabled = false;
    });

    // Begin ping process and listen for output
    final ping = Ping(_urlOrIP, count: 5);
    ping.stream.listen(
    (event) {
    _pingResultList.add("${ping.command} : ${event.toString()}");

    _controller.sink.add(_pingResultList);
    },
    onDone: () {
    setState(() {
    _userInputTextFieldEnabled = true;
    });
    },
    );
    },
    ),
    );

    return Scaffold(
    appBar: AppBar(
    title: const Text("Ping"),
    ),
    body: Column(
    children: [
    userInputTextField,
    StreamBuilder(
    initialData: _pingResultList,
    stream: _controller.stream,
    builder: (context, n) {
    List<String> results = n.data as List<String>;
    return Expanded(
    child: ListView(
    controller: _scrollController,
    children: [
    ...results.map((pingResult) => PingResultComponent(pingResult)).toList()
    ]
    )
    );
    }
    ),
    ]
    ),
    );
    }

    @override
    void dispose() {
    super.dispose();
    _controller.close();
    }
    }

    components/ping_result_component.dart

    import 'package:flutter/material.dart';

    // ignore: must_be_immutable
    class PingResultComponent extends StatefulWidget {
    String pingResult;

    PingResultComponent(this.pingResult, {super.key});

    @override
    State createState() {
    return _PingResultComponent();
    }
    }

    class _PingResultComponent extends State<PingResultComponent> {
    @override
    Widget build(BuildContext context) {
    Widget pingResultValueText =
    Flexible(
    child: Text(
    widget.pingResult,
    )
    );

    return Card(
    child: ListTile(
    title: Row(
    children: [
    pingResultValueText,
    ]
    ),
    ),
    );
    }
    }
  • Flutter Foreground Task 常駐程式套件

    Flutter Foreground Task 常駐程式套件

    在 Flutter 官方經營的 https://pub.dev 上有相當多好用的程式套件,其中,Flutter Foreground Task 是許多人推薦能在 Android 和 iOS 實現[常駐程式]機制的套件。

    pub.dev 上幾乎所有套件都會提供 Example 範例程式,以展示其使用方式及效果。Flutter Foreground Task 的範例程式的展示效果如下圖所示。

    左邊的為程式主畫面,按下[start]按鈕後便會執行常駐程式,並手機最上方的區域顯示程式的 icon (時間右邊數來第二個);右邊的為手機滑下上方區域的畫面,flutter_oreground_task_example 為程式名稱、MyTaskHandler 為固定顯示的內容、eventCount 為動態更新的內容、 按下Send 和 Test 按鈕可觸發程式做動作(似乎是 Android限定)、整個區塊按下去可跳回程式。

    由於該範例程式極具參考價值,這裡做一張對應的程式流程圖以方便理解參考。

    另外,關於前景服務的概念,也可以參考 Domen Lanisnik 的文章(Android):https://medium.com/@domen.lanisnik/guide-to-foreground-services-on-android-9d0127dc8f9a

  • Flutter 常用指令

    Flutter 常用指令

    以下為命令提示字元或VS Code的終端機介面下,常用的指令:

    • flutter {command} -h:查閱各command指令的用法和參數說明
    • flutter –version:檢視 flutter 版本
    • flutter doctor:檢查 flutter 開發環境相關軟體的安裝狀態
    • flutter upgrade:更新 flutter SDK 到最新版本 (也可以直接到 Flutter 官網下載)
    • flutter pub get:取得 package 並下載到 .pub-cache 隱藏目錄
    • flutter pub upgrade:更新 package 到最新版本
    • flutter clean:清除 .pub-cache 隱藏目錄。
    • flutter devices:顯示可用裝置資訊
    • flutter emulators:顯示可用模擬器資訊
    • flutter config –android-studio-dir=”{path}”:手動指定 Android Studio 的安裝路徑
    • flutter config –android-sdk=”{path}”:手動指定 Android SDK 的安裝路徑
    • flutter build apk:建立 release 版本 apk
    • flutter build apk –debug:建立 debug版本 apk

    如果Flutter專案目錄有異動,在編譯程式時可能會出現cache路徑錯誤之類的異常。這時候可以先清除 cache:flutter clean
    然後再重新取得 package 來排除問題:flutter pub get

  • Dart 程式語言命名原則

    Dart 程式語言命名原則

    • 資料夾和檔案名稱
      使用小寫英數字加底線,例如:models\customer_model.dart
    • Class 類別名稱
      使用 Upper Camel Case 大駝峰式命名,例如:class CustomerModel
    • 分辨 public 和 private
      類別的屬性和方法名稱若有底線,則代表 private,反之則為 public,例如:class _CustomerModel
      (Dart 沒有 public 和 private 關鍵字)
    • 變數和函數名稱
      使用 Lower Camel Case 小駝峰式命名,例如:int customerId = -1;
    • 建立新物件時可省略 new 這關鍵字
      例如:return new CustomerModel(); 可以寫成 return CustomerModel();
  • Flutter Structure Chart 架構圖

    下圖為 Flutter SDK 的架構。

  • Dart 的物件繼承

    Dart 的物件繼承

    • extends 繼承單一物件
    • implements 繼承且覆寫所有方法 (Dart 沒有提供 interface 的功能,但可透過 implements 達到相同目的)
    • mixins 繼承多個物件(多重繼承)
  • 解決 Flutter 編譯 Windows 程式出錯 Exception: Building with plugins requires symlink support

    在編譯成 Windows 程式時,若偵錯主控台出現如下般錯誤:

    Launching lib\main.dart on Windows in debug mode...
    Exception: Building with plugins requires symlink support.

    Please enable Developer Mode in your system settings. Run
    start ms-settings:developers
    to open settings.

    則可循該錯誤訊息的指示,開啟 Windows 的開發者模式,以便其能順利執行偵錯

    1. [Win]+[R] 開啟執行視窗,輸入
    ms-settings:developers

    2. 啟用[開發人員模式]即可

  • Flutter Web Service Client Sample 範例 (續) – 可編輯的表格內容

    Flutter Web Service Client Sample 範例 (續) – 可編輯的表格內容

    主要差異為 DataCell 內的 Text 改成可編輯的 TextField。

    components/post_table_with_search_and_edit.dart

    import 'dart:convert';
    import 'package:flutter/material.dart';
    import 'package:webservice_client/daos/shared_preferences_dao.dart';
    import 'package:webservice_client/models/post.dart';
    import 'package:http/http.dart' as http;

    // ignore: must_be_immutable
    class PostTableWithSearchAndEdit extends StatefulWidget {
    PostTableWithSearchAndEdit(this.posts, {super.key});
    List<Post> posts;

    @override
    State createState() {
    return _PostTableWithSearchAndEdit();
    }
    }

    class _PostTableWithSearchAndEdit extends State<PostTableWithSearchAndEdit> {
    String filterInput = '';
    List<Post> filteredPosts = [];

    void changeFilteredPosts(String userInput) {
    filterInput = userInput;
    filteredPosts = widget.posts.where(
    (element) {
    if(userInput == '') {
    return true;
    }
    else if(element.title.contains(userInput)) {
    //print(element.title);
    return true;
    }
    else {
    return false;
    }
    },
    ).toList();

    if(filteredPosts.isEmpty) {
    filteredPosts.add(Post(0, 0, "查無資料", "查無資料"));
    }

    SharedPreferencesDao.setSearchKey(userInput);
    }

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

    filterInput = SharedPreferencesDao.getSearchKey();
    }

    @override
    Widget build(BuildContext context) {

    if(filteredPosts.isEmpty) {
    changeFilteredPosts(filterInput);
    }

    var searchTextEditingController = TextEditingController()..text = filterInput;

    Widget searchBar = TextField(
    controller: searchTextEditingController,
    decoration: const InputDecoration(
    border: OutlineInputBorder(),
    hintText: 'Enter a search term'
    ),
    onSubmitted: (userInput){
    setState(() {
    changeFilteredPosts(userInput);
    });
    },
    );

    // Convert object variables to data column list
    List<String> columnNames = (jsonDecode(filteredPosts[0].toJsonObjectString()) as Map<String, dynamic>).keys.toList();
    List<DataColumn> dataColumns = columnNames.map((key) {
    return DataColumn(label: Text(key));
    },).toList();

    // Convert object variables to data row list
    List<DataRow> dataRows = filteredPosts.map((post) {
    Map<String, dynamic> postJson = jsonDecode(post.toJsonObjectString()) as Map<String, dynamic>;
    List<DataCell> dataCells = columnNames.map((key){
    return DataCell(
    TextField(
    controller: TextEditingController(text: postJson[key].toString()),
    onSubmitted: (inputStr){
    postJson[key] = inputStr;

    // Post Data
    var url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
    var res = http.post(url, body: jsonEncode(postJson));
    res.then((value) => print(value.body),);
    },
    ));
    }).toList();

    return DataRow(cells: dataCells);
    },).toList();

    return SingleChildScrollView(
    child: Container(
    alignment: Alignment.topCenter,
    child: Column(children: [
    SizedBox(
    width: 800,
    child: searchBar,
    ),
    SizedBox(
    width: 800,
    child: DataTable(columns: dataColumns, rows: dataRows),
    )
    ]),
    ),
    );
    }
    }
  • 關於 Flutter 的 StreamBuilder

    關於 Flutter 的 StreamBuilder

    在 Flutter 中,StreamBuilder 是一個非常有用的 widget,它可以根據與 Stream 的最新交互快照自我構建。這個 widget 特別適合於需要即時更新內容的應用場景,如聊天應用、社交網絡等。

    StreamBuilder 的基本用法如下:

    StreamBuilder(
    stream: stream, // 你的 Stream 對象
    builder: (BuildContext context, AsyncSnapshot snapshot) {
    // 這裡是 UI 構建邏輯,可以根據 snapshot 的數據來決定顯示什麼
    },
    )

    當提供一個 Stream 給 StreamBuilder時,它會監聽 Stream 的事件。每當其發出一個新的數據項目,builder 函數就會被調用,並且 AsyncSnapshot 會包含新的數據信息。這樣便可根據數據的變化來更新 UI。

    如果 Stream 發出一個錯誤,AsyncSnapshot 會帶有錯誤信息,我們可以決定如何處理這個錯誤(比如顯示一個錯誤提示)。當 Stream 完成所有事件的發送,AsyncSnapshot 的連接狀態會變為 ConnectionState.done,這時也可以進行相應的處理。

    另外,Stream和另一個也用來處理非同步事件的 Future 比,其差異如下:

    1. 數據傳遞
      • Future 代表一個將來某時可能會變得可用的單一結果或錯誤。當結果可用時,接收者可以註冊回調來處理該值或錯誤。
      • Stream 提供一個事件的連續流,這些事件隨著時間變化而變化,我們可以對這些變化作出反應。
    2. 訂閱
      • Future 只能被消費一次,當它完成時,便不能再從它那裡獲取更多的數據。
      • Stream 可以有多個訂閱者,這意味著多個組件可以從同一個流接收更新。

    如果需要一個一次性的非同步結果,使用 Future 及對應的 FutureBuilder ,如果需要監聽一個隨時間變化的數據序列,則就用Stream 及對應的 StreamBuilder。