2023年1月14日 星期六

Flutter學習-如何從firebase Cloud Firestore取值


一、Firestore結構認識
在說明firebase Cloud Firestore如何取值前,要先了解Firestore的資料表儲存結構,它有collection(集合),集合下面為Document(文件),文件下面為欄位。如下:





於Document(文件)下,可以增加欄位,儲存Map類型的資料<String,Dymatic>,也可以再新增集合,一直擴增下去。但文件下面不能再新增文件。



二、取值的方法

下面介紹幾種常用的取值方法:

不論用什麼方法取值,一開始都需要先取得實體,下面將FirebaseFirestore實體化,並以db為變數名稱

FirebaseFirestore db = FirebaseFirestore.instance;

向firebaes取值,受限於網路傳輸需要時間,寫函式時,需記得於函式後加上async採用異步的操作,而在需要異步回傳的程式前加入await 如下:

void getData() async{

var data= await .....

}

一、如果需要一次性的取得資料,可以使用get()方法。官方作法參考

(一)透過集合名稱,取得集合下的全部資料

取得的格式如下:

FirebaseFirestore.instance.collection("集合名").get();
以上面quiz_questions集合的資料為例:
QuerySnapshot<Map<String, dynamic>> data =FirebaseFirestore.instance.collection("quiz_questions").get();

用上面方法,取出的資料類型為QuerySnapshot<Map<String, dynamic>>,接著來看一下QuerySnapshot這個類別的方法,它為一個抽象類別,寫法如下:

abstract class QuerySnapshot<T extends Object?> {
/// Gets a list of all the documents included in this snapshot.
List<QueryDocumentSnapshot<T>> get docs;

/// An array of the documents that changed since the last snapshot. If this
/// is the first snapshot, all documents will be in the list as Added changes.
List<DocumentChange<T>> get docChanges;

/// Returns the [SnapshotMetadata] for this snapshot.
SnapshotMetadata get metadata;

/// Returns the size (number of documents) of this snapshot.
int get size;
}

QuerySnapshot裡面有四個方法,可以取得不同的資料。
(1)docs
docs可取得此snapshot(傳進來的資料快照)中包含的所有文件清單資料。也是一般最常用來取得集合下資料的方法。
以上面的例子就是取得index1 index2....等資料,另外,透過此取得的資料的類型為List<QueryDocumentSnapshot<T>>陣列,因此若要取得這些的文件的第一筆可以採取docs[0],第二筆為docs[1],第三筆為docs[2]......其它依此類推。另外,也可以透過其它List的方法,進行後續其它資料的處理。
(1-1)取得某序位文件下的全部資料 docs[0].data()
如果要更進一步取得第一筆文件下的全部資料,可以在docs[序位]後,加上data()
如:docs[0].data();
(1-2)取得某序位文件下的某一個欄位的值 ,可以在docs[序位]後,加上data(),再加上['欄位key值']
docs[0].data()['option']
(1-3)取得此文件名稱
如果你的文件名是亂數產生,在使用時,不知道文件名為何,可以透過id這個方法來取得,可以在docs[序位]後,加上id
如:要取得第一筆文件的文件名稱就是data.docs[0].id
(2)size
可取得這個集合下面的文件數量
(3)docChanges
取得這個集合下改變的資料,而如果是第一次新增,則會顯示新增的資料
(4)metadata
取得這個集合下原始的資料

以下為參考的範例
//取得quiz_questions集合裡的資料
QuerySnapshot<Map<dynamic, dynamic>> data =
await db.collection("quiz_questions").get();
//取得集合下第一個文件名
print(data.docs[0].id);
//取得集合下第一個文件下的所有欄位
print(data.docs[0].data());
//取得集合下第一個文件名下欄位名為options的值
print(data.docs[0].data()!['options']);

另外,如果要獲得關於集合這一層的資料,像是集合的名稱,更上一層父層的內容,可以透過下列的寫法:
CollectionReference<Map<String, dynamic>> CollectionRef =await db.collection("quiz_questions");

所取得的CollectionRef變數類型為CollectionReference<Map<String, dynamic>>,CollectionReference這個類別的寫法如下:

abstract class CollectionReference<T extends Object?> implements Query<T> {
/// Returns the ID of the referenced collection.
String get id;

/// Returns the parent [DocumentReference] of this collection or `null`.
///
/// If this collection is a root collection, `null` is returned.
// This always returns a DocumentReference even when using withConverter
// because we do not know what is the correct type for the parent doc. @override
DocumentReference<Map<String, dynamic>>? get parent;

/// A string containing the slash-separated path to this CollectionReference
/// (relative to the root of the database).
String get path;

/// Returns a `DocumentReference` with an auto-generated ID, after
/// populating it with provided [data].
///
/// The unique key generated is prefixed with a client-generated timestamp
/// so that the resulting list will be chronologically-sorted.
Future<DocumentReference<T>> add(T data);

/// {@template cloud_firestore.collection_reference.doc}
/// Returns a `DocumentReference` with the provided path.
///
/// If no [path] is provided, an auto-generated ID is used.
///
/// The unique key generated is prefixed with a client-generated timestamp
/// so that the resulting list will be chronologically-sorted.
/// {@endtemplate}
DocumentReference<T> doc([String? path]);

/// Transforms a [CollectionReference] to manipulate a custom object instead
/// of a `Map<String, dynamic>`.
///
/// This makes both read and write operations type-safe.
///
/// ```dart
/// final modelsRef = FirebaseFirestore
/// .instance
/// .collection('models')
/// .withConverter<Model>(
/// fromFirestore: (snapshot, _) => Model.fromJson(snapshot.data()!),
/// toFirestore: (model, _) => model.toJson(),
/// );
///
/// Future<void> main() async {
/// // Writes now take a Model as parameter instead of a Map
/// await modelsRef.add(Model());
///
/// // Reads now return a Model instead of a Map
/// final Model model = await modelsRef.doc('123').get().then((s) => s.data());
/// }
/// ```
// `extends Object?` so that type inference defaults to `Object?` instead of `dynamic`
@override
CollectionReference<R> withConverter<R extends Object?>({
required FromFirestore<R> fromFirestore,
required ToFirestore<R> toFirestore,
});
}

前三個為直接可以獲取的屬性:
(1)id 
回傳值為此集合的名稱,在上面的例子就是quiz_questions

建續上面的範例,CollectionRef為所設的變數,寫法如下:
CollectionRef.id

(2)parent

回傳值為上一層的DocumentReference,但如果此集合是最上層就會回傳null
CollectionRef.parent

(3)path
回傳一串,此字串 CollectionReference。如果是在最上層的root位置,就是回傳集合的名稱
CollectionRef.path
另外,CollectionReference下還有add()、doc()和withConverter()三個方法,add為增加文件資料和doc為指定文件路徑(可見下面第三項),而withConverter則是在取值時直接序列化及反序列化。


(二)透過集合名稱及文件名稱,取得文件下的資料

使用格式如下:

DocumentSnapshot<Map<String, dynamic>> data =
await db.collection("集合名").doc('文件名').get();

以上面quiz_questions集合的資料為例:
DocumentSnapshot<Map<String, dynamic>> data2 =
await db.collection("quiz_questions").doc('index2').get();

//取得quiz_questions集合裡文件名為index2的資料
DocumentSnapshot<Map<String, dynamic>> data2 =
await db.collection("quiz_questions").doc('index2').get();

//印出index2文件下所有的欄位
print(data2.data());
//印出index2文件下optinos這個欄位的值,data()有可能空,因此要加上不為空的判斷
print(data2.data()!['options']);


這裡要注意的是這裡的collection('集合名')是CollectionReference類別,而此類裡的方法是透過doc讀取下一層的資料,而不是get()方法下的QuerySnapshot類別,因此要注意不要加s,取出來變數類別為DocumentSnapshot<Map<String, dynamic>>,DocumentSnapshot這個類的程式如下:

abstract class DocumentSnapshot<T extends Object?> {
/// This document's given ID for this snapshot.
String get id;

/// Returns the reference of this snapshot.
DocumentReference<T> get reference;

/// Metadata about this document concerning its source and if it has local
/// modifications.
SnapshotMetadata get metadata;

/// Returns `true` if the document exists.
bool get exists;

/// Contains all the data of this document snapshot.
T? data();

/// {@template firestore.documentsnapshot.get}
/// Gets a nested field by [String] or [FieldPath] from this [DocumentSnapshot].
///
/// Data can be accessed by providing a dot-notated path or [FieldPath]
/// which recursively finds the specified data. If no data could be found
/// at the specified path, a [StateError] will be thrown.
/// {@endtemplate}
dynamic get(Object field);

/// {@macro firestore.documentsnapshot.get}
dynamic operator [](Object field);
}
(1)id

此文件的名稱 
print(data2.id); //印出為index1

(2)reference

取得指向的目標   
print(data2.reference);  //印出為DocumentReference<Map<String, dynamic>>(quiz_questions/index1)

(3)exists
文件是否存在
print(data2.exists);  //印出為true

(4)data()  <=有( )
取得該文件下的欄位及值對應所有資料,這與最前面(一)中,先取得全部集合資料,再透過文件於陣列的序位,取得下面的欄位資料相似。只是一個是以文件的名稱,而另一個是依文件在陣列中的序位順序取得。

print(data2.data());
//印出為{unitName: index1, answer: 1, options: 這是第1個問題, id: 1, time: Timestamp(seconds=1674980570, nanoseconds=575000000), answer_notes: 2345, quiz_name: 123}



(三) 透過集合名稱,並透過where()這個方法,有條件的取得集合下的資料

//取得quiz_questions集合裡文件名為index2的資料,並透過where()選擇 answer2的值
final data3 = await db
.collection("quiz_questions")
.where("answer", isEqualTo: "2")
.get();
//取得集合的數目
print(data3.size);
//取得集合下第一個文件名
print(data3.docs[0].id);
//取得集合下第一個文件名下欄位名為options的值
print(data3.docs[0].data()!['options']);
這裡可以使用where這個方法是因為CollectionReference 類implements 了Query這個類,因此可以採用Query 下所屬的where的方法。比較的方法如下,可參考上面的那個範例進行修改變
Query<T> where(
Object field, {
Object? isEqualTo, //相等於就回傳
Object? isNotEqualTo, //不相等於就回傳
Object? isLessThan, //少於就回傳
Object? isLessThanOrEqualTo, //少於或是相等於就回傳
Object? isGreaterThan, //大於就回傳
Object? isGreaterThanOrEqualTo, //大於或是相等於就回傳
Object? arrayContains, //包含某個值就回傳
List<Object?>? arrayContainsAny, //包含這個陣列中的某個值就回傳
List<Object?>? whereIn, //
List<Object?>? whereNotIn, //不包含在此陣列的值就回傳
bool? isNull,
});

使用arrayContains
範例:

Query query = Firestore.instance.collection('myCollection').where('myArrayField', arrayContains: 'myValue'); // myArrayField ['value1', 'value2', 'value3', 'myValue'] // myArrayField 'myValue'

使用arrayContainsAny

Query query = Firestore.instance.collection('myCollection').where('myArrayField', arrayContainsAny: ['value1', 'value2', 'myValue']); // myArrayField ['value1', 'value2', 'value3', 'myValue'] // myArrayField 'value1' 'value2' 'myValue'



(四)透過集合名稱,並透過orderBy()這個方法,以文件下欄位排序資料
基本格式:

var 變數名 =
await db.collection("集合名稱").orderBy("欄位名稱", descending: true).get();

上面的descending預設為false(升序排列),改為true為降序排列。

var data3 =
await db.collection("quiz_questions").orderBy("id", descending: true).get();

可以透過下面的方法將排序好的資料,印出來
for(int i=0;i<data3.docs.length;i++){
print(data3.docs[i].data());
}


(五)透過withConverter()來進行序列化及反序列化。

前面提到CollectionReference類下,有一個withConverter()方法,可以用來進行序列化及反序列化。要使用withConverter(),需先做一個Model來進行序列化及反序列化。
import 'package:cloud_firestore/cloud_firestore.dart';

class QuizModel2 {
//變數宣告
int? id;
String? unitName;
String? quizName;
String? options;
String? answer;
String? answer_notes;

// 該類的建構子
QuizModel2({
this.id,
this.unitName,
this.quizName,
this.options,
this.answer,
this.answer_notes,
});

//透過fromJson這個方法,將取得的json資料轉換成QuizModel
//QuizModel.id 等於 json["id"] ,那在使用這個類別的時候就不用背id
// QuizModel.,就會自動出現 id unitName等屬性名字可以選擇,減少出錯的機會

factory QuizModel2.fromJson(DocumentSnapshot<Map<String, dynamic>> snapshot,SnapshotOptions? options,){
final json = snapshot.data();
return QuizModel2(
id: json?["id"]== null ? null : json?["id"],
unitName: json?["unitName"]== null ? null : json?["unitName"],
quizName: json?["quizName"] == null ? null : json?["quizName"],
options: json?["options"] == null ? null : json?["options"],
answer: json?["answer"] == null ? null : json?["answer"],
answer_notes: json?["answer_notes"] == null ? null : json?["answer_notes"],
);
}

//這裡是進行反序列化,在存回資料庫时,要將資料變為{"id":,"unitName":}的形式,才能符合json的格式
Map<String, dynamic> toJson() => {
"id": id == null ? null : id,
"unitName": unitName == null ? null : unitName,
"quizName": quizName == null ? null : quizName,
"options": options == null ? null : options,
"answer": answer == null ? null : answer,
"answer_notes": answer_notes == null ? null : answer_notes,
};
}


 //quiz_questions為你的欄位名稱,index1為你的文件名稱
//fromFirestore後面放的是你的Model轉成序列化的方法
//toFirestore後面放的是你轉回序列化的方法
var data4 = await db
.collection('quiz_questions').doc('index1')
.withConverter(fromFirestore: QuizModel2.fromJson, toFirestore: (QuizModel2 quizModel2, _) => quizModel2.toJson())
.get();

// 透過上面的方法就可以開始使用 data4.data()?.id;

上面get()是一次性取得,但如果需要監聽資料是否有變化,並持續性的取得資料,可以透過StreamBuilder的方法,方法與上面的方法類似。但要記得使用StreamBuilder時,於builder中的snapshot為回傳的資料流,在用snapshot.hasData確定取得資料後,要進一步使用資料時,要先使用snapshot.data,這裡的.data與上面DocumentReference類的. data()方法不同,這個.data是AsyncSnapshot自己的屬性,是用來取得快照資料的方法,沒有加()。筆者一開始在使用時,因為沒有弄懂data和data()使用時機的不同導致一直出錯。

使用StreamBuilder取得上述資料,並利用Listview將資料顯示於畫面上的寫法如下:
StremBuilder的使用方法若不了解,可以點選此文章

StreamBuilder getFirebaseData() {
return StreamBuilder (
//資料流來源為FirebaseFirestore.instance.collection("quiz_questions").snapshots()
stream:
FirebaseFirestore.instance.collection("quiz_questions").snapshots(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
//沒有資料時,回傳CircularProgressIndicator(),有資料才進行更下面動作
if (!snapshot.hasData)
return Center(
child: CircularProgressIndicator(), //顥示圈圈的進度條
);
// 利用data將快照資料流裡的資料取出,此時的collectionDataquiz_questions這個集合下面的全部資料
var collectionData=snapshot.data;
//加上.docs,此時的documentDataquiz_questions下的文件集合,list[index1,index2.....]
//一般會透過for迴圈,或是listviewgridview才能將個別的資料一個一個取出。
//documentref[0]為第一筆,documentref[1]為第二筆....依此類推
var documentRef=collectionData.docs;
//若要取得文件長度,可用陣列的方法.length
final int documentCount = documentRef.length;
//假如有讀出資料時,就進行資料呈現
if (documentCount > 0) {
return ListView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
//itemCountlistView的總數目,這裡設為與文件的數量等長
itemCount: documentCount,
//利用listViewindex依序遍歷文件
itemBuilder: (_, int index) {
//index0時,為取得quiz_questions下第一個文件
var documentData=documentRef[index];
//如果需要序列化與反序列化可以在這裡進行
var documentData_fromjson=QuizModel.fromJson(documentData.data() as Map<String, dynamic>);
//序列化後可使用.的方式使用變數 如:documentData_fromjson.answer;
//如果不需要序列化可以用map鍵值對的方法取出欄位中的值 documentData['欄位名']
return Text(documentData['options'],style: TextStyle(fontSize: 20));
},
);
//沒有資料的話做以下動作
} else {
return Container(
padding: EdgeInsets.symmetric(vertical: 10.0),
alignment: Alignment.center,
child: Text(
'未取得資料',
style: TextStyle(fontSize: 20),
),
);
}
},
);
}
上面使用的Model的寫法與withConverter()不同,也附在下面:

class QuizModel {
//變數宣告
int? id;
String? unitName;
String? quizName;
String? options;
String? answer;
String? answer_notes;

// 該類的建構子
QuizModel({
this.id,
this.unitName,
this.quizName,
this.options,
this.answer,
this.answer_notes,
});

//透過fromJson這個方法,將取得的json資料轉換成QuizModel
//QuizModel.id 等於 json["id"] ,那在使用這個類別的時候就不用背id
// QuizModel.,就會自動出現 id unitName等屬性名字可以選擇,減少出錯的機會

factory QuizModel.fromJson(Map<String, dynamic> json){
return QuizModel(
id: json["id"]== null ? null : json["id"],
unitName: json["unitName"]== null ? null : json["unitName"],
quizName: json["quizName"] == null ? null : json["quizName"],
options: json["options"] == null ? null : json["options"],
answer: json["answer"] == null ? null : json["answer"],
answer_notes: json["answer_notes"] == null ? null : json["answer_notes"],
);
}

//這裡是進行反序列化,在存回資料庫时,要將資料變為{"id":,"unitName":}的形式,才能符合json的格式
Map<String, dynamic> toJson() => {
"id": id == null ? null : id,
"unitName": unitName == null ? null : unitName,
"quizName": quizName == null ? null : quizName,
"options": options == null ? null : options,
"answer": answer == null ? null : answer,
"answer_notes": answer_notes == null ? null : answer_notes,
};
}

沒有留言:

張貼留言