การใช้ GraphQL แทน REST API ใน Flutter
ในบทความก่อนหน้า เราได้เรียนรู้เกี่ยวกับการสร้าง Flutter Plugin เพื่อเข้าถึง Native code ผ่าน Platform Channels ในบทความนี้ เราจะมาสำรวจอีกหนึ่งทางเลือกในการจัดการข้อมูล นั่นคือการใช้ GraphQL แทน REST API ใน Flutter ซึ่งจะช่วยให้การดึงข้อมูลมีประสิทธิภาพมากยิ่งขึ้น
REST API เป็นสถาปัตยกรรมที่ใช้กันอย่างแพร่หลายในการสร้าง API แต่ก็มีข้อจำกัดบางอย่าง เช่น การดึงข้อมูลที่มากเกินความจำเป็น (over-fetching) หรือการดึงข้อมูลหลายครั้งเพื่อรวบรวมข้อมูลที่ต้องการ (under-fetching) GraphQL เข้ามาแก้ปัญหาเหล่านี้ด้วยการให้ client ระบุข้อมูลที่ต้องการได้อย่างแม่นยำ ทำให้การดึงข้อมูลมีประสิทธิภาพและยืดหยุ่นมากขึ้น
ทำไมต้อง GraphQL?
- Over-fetching และ Under-fetching: REST API มักจะส่งคืนข้อมูลมากกว่าที่ client ต้องการ หรือ client อาจต้องทำการ request หลายครั้งเพื่อรวบรวมข้อมูลที่ต้องการ GraphQL แก้ปัญหานี้ด้วยการให้ client ระบุ field ที่ต้องการได้อย่างชัดเจน
- Strong Typing: GraphQL มีระบบ type ที่แข็งแกร่ง ทำให้การตรวจสอบความถูกต้องของข้อมูลทำได้ง่ายขึ้น และช่วยลดข้อผิดพลาดที่อาจเกิดขึ้นใน runtime
- Introspection: GraphQL schema สามารถ query ได้ ทำให้ client สามารถ discover API ได้ง่ายขึ้น
- Single Endpoint: GraphQL มักจะมี endpoint เดียวสำหรับการ query และ mutation ทำให้การจัดการ API ง่ายขึ้น
GraphQL Client ใน Flutter
ในการใช้งาน GraphQL ใน Flutter เราจำเป็นต้องใช้ GraphQL client library ที่จะช่วยให้เราสามารถส่ง GraphQL queries และ mutations ไปยัง server ได้ มี library หลายตัวให้เลือกใช้ แต่ที่นิยมกันคือ:
- graphql_flutter: เป็น library ที่ครอบคลุมและใช้งานง่าย รองรับทั้ง query, mutation, subscription และ caching
- ferry: เป็น library ที่เน้นประสิทธิภาพและ scalability เหมาะสำหรับแอปพลิเคชันขนาดใหญ่
ในบทความนี้ เราจะใช้ graphql_flutter เนื่องจากเป็น library ที่เริ่มต้นใช้งานได้ง่าย
การติดตั้ง graphql_flutter
เพิ่ม dependency graphql_flutter ลงใน pubspec.yaml:
dependencies:
flutter:
sdk: flutter
graphql_flutter: ^5.1.0 # ตรวจสอบ version ล่าสุด
จากนั้นทำการ run flutter pub get เพื่อติดตั้ง dependency
การตั้งค่า GraphQL Client
เริ่มต้นด้วยการสร้าง GraphQLClient instance โดยระบุ GraphQL endpoint:
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final HttpLink httpLink = HttpLink(
'https://rickandmortyapi.graphcdn.app/', // ตัวอย่าง GraphQL endpoint
);
final ValueNotifier<GraphQLClient> client = ValueNotifier(
GraphQLClient(
link: httpLink,
cache: GraphQLCache(store: HiveStore()),
),
);
var app = MaterialApp(
title: 'GraphQL Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: GraphQLProvider(
client: client,
child: const MyApp(),
),
);
runApp(app);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('GraphQL Demo'),
),
body: Center(
child: Text('GraphQL Demo'),
),
);
}
}
ในโค้ดนี้ เราได้สร้าง GraphQLClient instance โดยระบุ endpoint เป็น https://rickandmortyapi.graphcdn.app/ ซึ่งเป็น GraphQL API สำหรับข้อมูล Rick and Morty นอกจากนี้เรายังใช้ GraphQLProvider เพื่อให้ client สามารถเข้าถึงได้จากทุก widget ใน widget tree
Queries
Queries ใช้สำหรับดึงข้อมูลจาก GraphQL server ตัวอย่างเช่น เราต้องการดึงรายชื่อตัวละครทั้งหมดจาก Rick and Morty API:
query GetCharacters {
characters {
results {
id
name
status
species
}
}
}
เราสามารถใช้ Query widget จาก graphql_flutter เพื่อ execute query นี้:
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
class CharacterList extends StatelessWidget {
const CharacterList({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Query(
options: QueryOptions(
document: gql(
'''
query GetCharacters {
characters {
results {
id
name
status
species
}
}
}
''',
),
),
builder: (QueryResult result, {VoidCallback? refetch, FetchMore? fetchMore}) {
if (result.hasException) {
return Text(result.exception.toString());
}
if (result.isLoading) {
return const Center(child: CircularProgressIndicator());
}
final characterList = result.data?['characters']['results'];
if (characterList == null) {
return const Text('No characters found');
}
return ListView.builder(
itemCount: characterList.length,
itemBuilder: (context, index) {
final character = characterList[index];
return ListTile(
title: Text(character['name']),
subtitle: Text('${character['status']} - ${character['species']}'),
);
},
);
},
);
}
}
ในโค้ดนี้ Query widget จะทำการ execute query และ render UI ตามสถานะของ query (loading, error, data)
Mutations
Mutations ใช้สำหรับแก้ไขข้อมูลบน GraphQL server (เช่น สร้าง, อัปเดต, ลบ) เนื่องจาก Rick and Morty API เป็น public API ที่อ่านได้อย่างเดียว (read-only) เราจะไม่สามารถแสดงตัวอย่าง mutation ได้ แต่หลักการใช้งานก็คล้ายคลึงกับ queries คือใช้ Mutation widget จาก graphql_flutter
สมมติว่าเรามี GraphQL API ที่อนุญาตให้สร้าง user ใหม่ เราสามารถเขียน mutation ได้ดังนี้:
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
}
}
และใช้ Mutation widget ใน Flutter:
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
class CreateUserForm extends StatefulWidget {
const CreateUserForm({Key? key}) : super(key: key);
@override
State<CreateUserForm> createState() => _CreateUserFormState();
}
class _CreateUserFormState extends State<CreateUserForm> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
@override
Widget build(BuildContext context) {
return Mutation(
options: MutationOptions(
document: gql(
'''
mutation CreateUser(\$name: String!, \$email: String!) {
createUser(name: \$name, email: \$email) {
id
name
email
}
}
''',
),
onCompleted: (dynamic result) {
// Handle successful mutation
print('User created: ${result?['createUser']['name']}');
},
onError: (error) {
// Handle error
print('Error creating user: $error');
},
),
builder: (RunMutation runMutation, QueryResult? result) {
return Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Name'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your name';
}
return null;
},
),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
runMutation({
'name': _nameController.text,
'email': _emailController.text,
});
}
},
child: const Text('Create User'),
),
if (result?.isLoading == true) const CircularProgressIndicator(),
],
),
);
},
);
}
}
ในโค้ดนี้ Mutation widget จะรับ RunMutation function ซึ่งเราสามารถเรียกใช้เพื่อ execute mutation โดยส่ง variables ที่จำเป็น (name และ email) ไปด้วย
ข้อผิดพลาดที่พบบ่อย
- GraphQL Endpoint ไม่ถูกต้อง: ตรวจสอบให้แน่ใจว่า GraphQL endpoint ที่ระบุถูกต้องและสามารถเข้าถึงได้
- Schema Mismatch: ตรวจสอบให้แน่ใจว่า query หรือ mutation ที่เขียนสอดคล้องกับ schema ของ GraphQL API
- Variables ไม่ถูกต้อง: ตรวจสอบให้แน่ใจว่า variables ที่ส่งไปยัง query หรือ mutation มี type ที่ถูกต้องและจำเป็นตามที่กำหนดไว้ใน schema
- Network Error: ตรวจสอบการเชื่อมต่ออินเทอร์เน็ต
สรุป
ในบทความนี้ เราได้เรียนรู้เกี่ยวกับการใช้ GraphQL แทน REST API ใน Flutter โดยใช้ graphql_flutter library เราได้เรียนรู้วิธีการตั้งค่า GraphQL client, การ execute queries และ mutations รวมถึงข้อผิดพลาดที่พบบ่อย การใช้ GraphQL ช่วยให้เราสามารถดึงข้อมูลได้อย่างมีประสิทธิภาพและยืดหยุ่นมากขึ้น ซึ่งเป็นประโยชน์อย่างมากในการพัฒนาแอปพลิเคชัน Flutter ที่ต้องการประสิทธิภาพสูง
ในบทความถัดไป เราจะมาเรียนรู้เกี่ยวกับการจัดการ dependency ด้วย GetX ซึ่งเป็น framework ที่ช่วยให้การจัดการ state, navigation และ dependency ใน Flutter ง่ายขึ้น