- extends 繼承單一物件
- implements 繼承且覆寫所有方法 (Dart 沒有提供 interface 的功能,但可透過 implements 達到相同目的)
- mixins 繼承多個物件(多重繼承)
Category: Tech
IT Technology, 3C, AI
-

Dart 的物件繼承
-
解決 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 範例 (續) – 可編輯的表格內容
主要差異為 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是一個非常有用的 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 比,其差異如下:
- 數據傳遞:
Future代表一個將來某時可能會變得可用的單一結果或錯誤。當結果可用時,接收者可以註冊回調來處理該值或錯誤。Stream提供一個事件的連續流,這些事件隨著時間變化而變化,我們可以對這些變化作出反應。
- 訂閱:
Future只能被消費一次,當它完成時,便不能再從它那裡獲取更多的數據。Stream可以有多個訂閱者,這意味著多個組件可以從同一個流接收更新。
如果需要一個一次性的非同步結果,使用 Future 及對應的 FutureBuilder ,如果需要監聽一個隨時間變化的數據序列,則就用Stream 及對應的 StreamBuilder。
- 數據傳遞:
-

關於 Flutter 的 FutureBuilder
在使用Flutter進行開發時,我們經常會遇到需要處理異步操作的情況,比如從網路上獲取數據或進行耗時的運算。為了在使用者介面上呈現異步操作的結果,Flutter提供了一個名為FutureBuilder的Widget。FutureBuilder是一個功能強大且靈活的工具,它能幫助我們簡化異步操作的處理,並能根據操作的狀態動態地構建使用者介面。
FutureBuilder是一個Widget,其功能是根據異步操作的狀態動態地構建使用者介面。它會接收一個Future物件作為輸入,並根據Future的不同狀態(未完成、完成、錯誤)來構建不同的使用者介面。FutureBuilder能夠將異步操作的結果直接用於構建使用者介面,無需手動管理狀態或訂閱事件。
FutureBuilder的運作方式相當直觀,它透過接收一個Future物件並監聽其狀態的變化來構建使用者介面。根據Future的狀態,FutureBuilder會調用不同的回調函數來構建對應的使用者介面:
- 當Future還未完成時,FutureBuilder會調用builder回調函數,並傳遞一個BuildContext和AsyncSnapshot物件。我們可以根據AsyncSnapshot的狀態來構建正在加載的使用者介面。
- 當Future成功完成時,FutureBuilder會調用builder回調函數,並傳遞一個BuildContext和AsyncSnapshot物件。我們可以根據AsyncSnapshot的狀態來構建成功完成時的使用者介面,並使用AsyncSnapshot的data屬性來訪問Future的結果數據。
- 當Future發生錯誤時,FutureBuilder會調用errorBuilder回調函數,並傳遞一個BuildContext和錯誤物件。我們可以根據錯誤物件來構建錯誤提示的使用者介面。
使用情境
獲取網路數據並構建使用者介面:FutureBuilder非常適合於從網路獲取數據並將其用於構建使用者介面的情境。我們可以在FutureBuilder的future參數中傳遞一個異步的網路請求,並根據請求的結果來構建不同的使用者介面,例如顯示正在加載的使用者介面、成功獲取數據後的使用者介面或錯誤提示的使用者介面。
進行異步計算和處理:除了獲取網路數據,FutureBuilder也適用於執行耗時的計算或其他異步操作,並根據操作結果來構建使用者介面。我們可以在future參數中傳遞一個異步的計算函數,並根據計算結果來構建相應的使用者介面。
與其他Widget結合使用:FutureBuilder可以與其他小部件組合使用,以實現更複雜的異步操作和使用者介面交互。例如,我們可以將FutureBuilder嵌入在ListView或GridView中,以構建一個根據異步數據生成的動態列表的使用者介面。
使用時的注意事項
避免頻繁重建使用者介面:由於FutureBuilder會根據異步操作的狀態動態地構建使用者介面,因此在每次狀態變化時都會觸發使用者介面的重建。為了避免不必要的重建,我們可以使用const關鍵字來標記不需要重新構建的部分,或者將FutureBuilder封裝在StatefulWidget中,通過維護狀態來控制使用者介面的重建。 錯誤處理和錯誤信息展示:FutureBuilder提供了errorBuilder回調函數,用於處理Future發生錯誤的情況。在構建錯誤提示的使用者介面時,我們應該提供有意義的錯誤信息,並考慮使用者體驗和友善的錯誤處理方式。
替代方案
雖然FutureBuilder是處理異步操作和構建使用者介面的常見工具,但在特定情況下,你可能會考慮使用其他的替代方案。以下是一些常見的替代方案:
StreamBuilder:如果你的異步操作返回的是一個數據流(Stream),而不只是一個Future,那麼可以使用StreamBuilder來處理。StreamBuilder與FutureBuilder相似,但用於處理數據流的更新。它監聽數據流的狀態變化,並根據流的狀態構建相應的使用者介面。 Provider和ChangeNotifier:如果你需要在異步操作中更新應用程式的狀態,並通知使用者介面進行相應的更新,可以使用Provider和ChangeNotifier。Provider是Flutter生態中的狀態管理工具,而ChangeNotifier是一個可觀察的模型,用於追蹤狀態的變化。你可以使用Future或其他異步操作來更新ChangeNotifier,並通過Provider將狀態提供給使用者介面組件。 StatefulWidget和State:如果你需要更細緻地控制異步操作和使用者介面的交互,可以使用StatefulWidget和State。你可以創建一個帶有狀態的小部件,將異步操作封裝在狀態中,並在狀態的生命週期方法中處理異步操作和使用者介面更新。 Reactive框架:在Flutter生態系統中,還有一些基於響應式編程的框架,例如ReactiveX和RxDart。這些框架提供了豐富的操作符和轉換器,用於處理異步操作和數據流。如果你對響應式編程有一定的了解,並且需要處理複雜的異步操作和數據流轉換,可以考慮使用這些框架。
需要注意的是,這些替代方案並不是FutureBuilder的直接替代品,而是根據具體的需求和情況選擇合適的工具和模式。FutureBuilder在處理簡單的異步操作和構建基本使用者介面時非常方便,但在複雜的場景下,其他方案可能更適合。根據你的項目需求和個人偏好,選擇最適合的工具和模式來處理異步操作和構建使用者介面。
-

Flutter Web Service Client Sample 範例 (續) – 自動儲存和載入最後一次查詢條件
承前一天的查詢方塊功能,透過 SharedPreferences 套件儲存和載入查詢的條件。讓 App開啟時自動載入最後一次的查詢條件。(此機制常用於儲存使用者的登入帳號密碼並自動登入)
daos/shared_preferences_dao.dart
import 'package:shared_preferences/shared_preferences.dart';
class SharedPreferencesDao {
static SharedPreferences? _prefs;
static void initialize() async {
_prefs ??= await SharedPreferences.getInstance();
}
static String getSearchKey() {
String? searchKey;
if(_prefs != null) {
searchKey = _prefs!.getString("searchKey");
}
searchKey ??= '';
return searchKey;
}
static void setSearchKey(String searchKey) {
if(_prefs != null) {
_prefs!.setString("searchKey", searchKey);
}
}
}daos/post_dao.dart
import 'package:webservice_client/models/post.dart';
import 'package:http/http.dart' as http;
import 'package:webservice_client/daos/shared_preferences_dao.dart';
import 'dart:convert';
class PostDao {
static Future<List<Post>> getPostsAndInitializeSharedPreferences() async {
SharedPreferencesDao.initialize();
var url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
var response = await http.get(url);
List<Post> posts = (jsonDecode(response.body) as List<dynamic>).map((jsonObject) {
return Post.fromJson(jsonObject);
}).toList();
return posts;
}
}components/post_table_with_search.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';
// ignore: must_be_immutable
class PostTableWithSearch extends StatefulWidget {
PostTableWithSearch(this.posts, {super.key});
List<Post> posts;
@override
State createState() {
return _PostTableWithSearch();
}
}
class _PostTableWithSearch extends State<PostTableWithSearch> {
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(
Text(postJson[key].toString()));
}).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),
)
]),
),
);
}
}screens/post_screen.dart
import 'package:flutter/material.dart';
import 'package:webservice_client/components/post_table_with_search.dart';
import 'package:webservice_client/daos/post_dao.dart';
import 'package:webservice_client/models/post.dart';
class PostScreen extends StatefulWidget {
const PostScreen({super.key});
@override
State createState() {
return _PostScreen();
}
}
class _PostScreen extends State<PostScreen> {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: PostDao.getPostsAndInitializeSharedPreferences(),
builder: (BuildContext context, AsyncSnapshot<List<Post>> asyncSnapshot) {
List<Post> posts = [];
if(asyncSnapshot.connectionState == ConnectionState.done) {
posts = asyncSnapshot.requireData;
return Scaffold(
body: PostTableWithSearch(posts),
);
}
else if(asyncSnapshot.hasError) {
return const Scaffold(body: Text('Initial data fail!'),);
}
else {
return const Scaffold();
}
}
);
}
}pubspec.yaml
name: webservice_client
description: "A http client sample project."
publish_to: 'none'
version: 0.1.0+1
environment:
sdk: '>=3.2.6 <4.0.0'
dependencies:
flutter:
sdk: flutter
http: ^0.13.4
shared_preferences: ^2.0.8
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true -

Flutter Web Service Client Sample 範例 (續) – 加上查詢方塊
針對前一個 Web Service Client 和表格呈現資料的範例,增加需要處理狀態更新機制(StatefulWidget)的查詢方塊。
components/post_table_with_search.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webservice_client/models/post.dart';
// ignore: must_be_immutable
class PostTableWithSearch extends StatefulWidget {
PostTableWithSearch(this.posts, {super.key});
List<Post> posts;
@override
State createState() {
return _PostTableWithSearch();
}
}
class _PostTableWithSearch extends State<PostTableWithSearch> {
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, "查無資料", "查無資料"));
}
}
@override
Widget build(BuildContext context) {
if(filteredPosts.isEmpty) {
changeFilteredPosts('');
}
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(
Text(postJson[key].toString()));
}).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),
)
]),
),
);
}
}screen/post_screen.dart
import 'package:flutter/material.dart';
import 'package:webservice_client/components/post_table_with_search.dart';
import 'package:webservice_client/daos/post_dao.dart';
import 'package:webservice_client/models/post.dart';
class PostScreen extends StatefulWidget {
const PostScreen({super.key});
@override
State createState() {
return _PostScreen();
}
}
class _PostScreen extends State<PostScreen> {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: PostDao.getPosts(),
builder: (BuildContext context, AsyncSnapshot<List<Post>> asyncSnapshot) {
return Scaffold(
body: PostTableWithSearch(asyncSnapshot.requireData),
);
}
);
}
} -

Flutter Web Service Client Sample 範例 (續) – 用DataTable 呈現資料
原範例是直接用文字的方式呈現,下面是改成透過 DataTable 元件以表格方式呈現。
(以下為新增和修改的 .dart 程式檔案)component/post_table.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webservice_client/models/post.dart';
// ignore: must_be_immutable
class PostTable extends StatelessWidget {
PostTable(this.posts, {super.key});
List<Post> posts;
@override
Widget build(BuildContext context) {
// Convert object variables to data column list
List<String> columnNames = (jsonDecode(posts[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 = posts.map((post) {
Map<String, dynamic> postJson = jsonDecode(post.toJsonObjectString()) as Map<String, dynamic>;
List<DataCell> dataCells = columnNames.map((key){
return DataCell(
Text(postJson[key].toString()));
}).toList();
return DataRow(cells: dataCells);
},).toList();
return DataTable(columns: dataColumns, rows: dataRows);
}
}screen/post_screen.dart
import 'package:flutter/material.dart';
import 'package:webservice_client/components/post_table.dart';
import 'package:webservice_client/daos/post_dao.dart';
import 'package:webservice_client/models/post.dart';
class PostScreen extends StatelessWidget {
const PostScreen({super.key});
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: PostDao.getPosts(),
builder: (BuildContext context, AsyncSnapshot<List<Post>> asyncSnapshot) {
return Scaffold(
body: PostTable(asyncSnapshot.requireData),
);
}
);
}
} -

Flutter Web Service Client Sample 範例
使用 FutureBuilder 搭配事先定義好的對應物件,透過非同步 http 的方式接收網路上的 Json 資料。
main.dart
import 'package:flutter/material.dart';
import 'package:webservice_client/screens/post_screen.dart';
void main() {
runApp(const AppEntryPoint());
}
class AppEntryPoint extends StatelessWidget {
const AppEntryPoint({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
"/post-screen":(context) => const PostScreen()
},
initialRoute: "/post-screen",
);
}
}screen/post_screen.dart
import 'package:flutter/material.dart';
import 'package:webservice_client/components/post_text.dart';
class PostScreen extends StatelessWidget {
const PostScreen({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: PostText(),
);
}
}components/post_text.dart
import 'package:flutter/material.dart';
import 'package:webservice_client/daos/post_dao.dart';
import 'package:webservice_client/models/post.dart';
class PostText extends StatelessWidget {
const PostText({super.key});
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: PostDao.getPosts(),
builder: (BuildContext context, AsyncSnapshot<List<Post>> asyncSnapshot) {
List<Widget> widgets = [];
//print(asyncSnapshot.connectionState);
//print(asyncSnapshot.hasData);
if(asyncSnapshot.connectionState == ConnectionState.done) {
widgets = asyncSnapshot.requireData.map((post){
return Text(post.toJsonObjectString());
}).toList();
}
return SingleChildScrollView(
child: Column(children: widgets),
);
},
);
}
}daos/post_dao.dart
import 'package:webservice_client/models/post.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class PostDao {
static Future<List<Post>> getPosts() async {
var url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
var response = await http.get(url);
List<Post> posts = (jsonDecode(response.body) as List<dynamic>).map((jsonObject) {
return Post.fromJson(jsonObject);
}).toList();
return posts;
}
}models/post.dart
import 'dart:convert';
class Post {
int userId;
int id;
String title;
String body;
Post(this.userId, this.id, this.title, this.body);
String toJsonObjectString(){
return jsonEncode({
"userId": userId,
"id": id,
"title": title,
"body": body
});
}
factory Post.fromJson(dynamic jsonObject) {
return Post(
jsonObject['userId'],
jsonObject['id'],
jsonObject['title'],
jsonObject['body'],
);
}
}
