การจัดการ Dependency ด้วย GetX ใน Flutter
ในบทความก่อนหน้า เราได้เรียนรู้เกี่ยวกับการใช้ GraphQL เพื่อจัดการข้อมูลในแอป Flutter ของเรา ซึ่งช่วยให้เราสามารถดึงข้อมูลที่ต้องการได้อย่างแม่นยำ ลดปริมาณข้อมูลที่ไม่จำเป็น และเพิ่มประสิทธิภาพของแอปพลิเคชัน ในบทความนี้ เราจะมาเรียนรู้เกี่ยวกับการจัดการ Dependency และ State ด้วย GetX ซึ่งเป็น library ที่ได้รับความนิยมอย่างมากใน Flutter เนื่องจากความสามารถในการจัดการ state, routes, dependency injection และ localization ได้อย่างง่ายดายและมีประสิทธิภาพ
GetX ไม่ได้เป็นเพียงแค่ state management library แต่เป็น microframework ที่มีเครื่องมือหลากหลายที่จะช่วยให้การพัฒนา Flutter app เป็นไปอย่างรวดเร็วและมีประสิทธิภาพมากขึ้น ในบทความนี้ เราจะเน้นไปที่การใช้งาน GetX ในการจัดการ Dependency ซึ่งเป็นส่วนสำคัญในการสร้างแอปพลิเคชันที่สามารถบำรุงรักษาได้ง่าย (maintainable) และสามารถทดสอบได้ (testable)
ทำไมต้องจัดการ Dependency?
ก่อนที่เราจะลงลึกในรายละเอียดของการใช้ GetX ในการจัดการ Dependency เรามาทำความเข้าใจก่อนว่าทำไมการจัดการ Dependency ถึงมีความสำคัญ การจัดการ Dependency ที่ดีจะช่วยให้:
- ลดความซับซ้อนของโค้ด: ลดการพึ่งพากันของคลาสต่างๆ ทำให้โค้ดอ่านง่ายและเข้าใจง่ายขึ้น
- เพิ่มความสามารถในการนำโค้ดกลับมาใช้ใหม่ (reusability): ทำให้คลาสต่างๆ สามารถนำไปใช้ในส่วนอื่นๆ ของแอปพลิเคชันได้ง่ายขึ้น
- เพิ่มความสามารถในการทดสอบ (testability): ทำให้การเขียน Unit test และ Integration test เป็นไปได้ง่ายขึ้น เนื่องจากเราสามารถจำลอง (mock) dependencies ได้ง่าย
- ลดข้อผิดพลาด: ลดโอกาสในการเกิดข้อผิดพลาดเนื่องจากการพึ่งพากันของคลาสต่างๆ
GetX Dependency Injection
GetX มีระบบ Dependency Injection ที่เรียบง่ายแต่ทรงพลัง ซึ่งช่วยให้เราสามารถจัดการ dependencies ในแอปพลิเคชันได้อย่างง่ายดาย GetX มีสองฟังก์ชันหลักที่ใช้ในการจัดการ dependencies คือ Get.put() และ Get.find()
Get.put()
Get.put() ใช้สำหรับ inject (หรือใส่) dependency เข้าไปใน GetX container ซึ่งจะทำให้ dependency นั้นสามารถเข้าถึงได้จากที่อื่นๆ ในแอปพลิเคชัน
ตัวอย่างการใช้งาน:
import 'package:get/get.dart';
class ApiService {
Future<String> fetchData() async {
// จำลองการดึงข้อมูลจาก API
await Future.delayed(Duration(seconds: 1));
return "Data from API";
}
}
class MyController extends GetxController {
final ApiService apiService;
MyController({required this.apiService});
RxString data = "".obs;
@override
void onInit() {
super.onInit();
fetchDataFromApi();
}
Future<void> fetchDataFromApi() async {
data.value = await apiService.fetchData();
}
}
void main() {
// Inject ApiService เข้าไปใน GetX container
Get.put(ApiService());
// Inject MyController เข้าไปใน GetX container โดยใช้ ApiService ที่ถูก inject ไปก่อนหน้า
Get.put(MyController(apiService: Get.find<ApiService>()));
// ... runApp(MyApp());
}
ในตัวอย่างนี้ เราได้สร้างคลาส ApiService ซึ่งจำลองการดึงข้อมูลจาก API จากนั้นเราสร้างคลาส MyController ซึ่งขึ้นอยู่กับ ApiService ในฟังก์ชัน main() เราได้ใช้ Get.put() เพื่อ inject ทั้ง ApiService และ MyController เข้าไปใน GetX container สังเกตว่าในการ inject MyController เราได้ใช้ Get.find<ApiService>() เพื่อดึง instance ของ ApiService ที่ถูก inject ไปก่อนหน้า
Get.find()
Get.find() ใช้สำหรับดึง instance ของ dependency ที่ถูก inject เข้าไปใน GetX container ก่อนหน้านี้
ตัวอย่างการใช้งาน:
import 'package:get/get.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ดึง instance ของ MyController จาก GetX container
final MyController controller = Get.find<MyController>();
return Scaffold(
appBar: AppBar(title: Text("GetX Dependency Injection")),
body: Center(
child: Obx(() => Text(controller.data.value)),
),
);
}
}
ในตัวอย่างนี้ เราได้สร้าง MyWidget ซึ่งต้องการเข้าถึง MyController เราสามารถดึง instance ของ MyController ได้อย่างง่ายดายโดยใช้ Get.find<MyController>()
Obx: Reactive State Management
GetX ไม่ได้มีดีแค่ Dependency Injection เท่านั้น แต่ยังมีระบบ State Management ที่เรียกว่า "Obx" ซึ่งย่อมาจาก "Observable". Obx ช่วยให้เราสามารถสร้าง Widget ที่ react (ตอบสนอง) ต่อการเปลี่ยนแปลงของ state ได้อย่างง่ายดาย
ตัวอย่างการใช้งาน:
import 'package:get/get.dart';
class MyController extends GetxController {
RxInt count = 0.obs;
void increment() {
count.value++;
}
}
class MyWidget extends StatelessWidget {
final MyController controller = Get.put(MyController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("GetX Obx")),
body: Center(
child: Obx(() => Text("Count: ${controller.count.value}")),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
controller.increment();
},
child: Icon(Icons.add),
),
);
}
}
ในตัวอย่างนี้ เราได้สร้าง MyController ซึ่งมี RxInt count เป็น observable state เมื่อเราเรียกฟังก์ชัน increment() ค่าของ count จะถูก update และ Widget ที่ wrapped ด้วย Obx() จะ rebuild ตัวเองโดยอัตโนมัติ
กรณีใช้งานจริงและข้อผิดพลาดที่พบบ่อย
กรณีใช้งานจริง: การจัดการ Authentication
ลองพิจารณากรณีที่เราต้องการจัดการ Authentication ในแอปพลิเคชันของเรา เราสามารถสร้าง AuthController เพื่อจัดการ authentication state และ inject เข้าไปใน GetX container จากนั้นเราสามารถเข้าถึง AuthController จากทุกที่ในแอปพลิเคชัน
import 'package:get/get.dart';
class AuthController extends GetxController {
RxBool isLoggedIn = false.obs;
Future<void> login(String username, String password) async {
// จำลองการ login
await Future.delayed(Duration(seconds: 1));
isLoggedIn.value = true;
}
void logout() {
isLoggedIn.value = false;
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp( // Use GetMaterialApp for GetX navigation
title: 'Flutter Demo',
home: HomePage(),
initialBinding: BindingsBuilder(() {
Get.put(AuthController());
}),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final AuthController authController = Get.find<AuthController>();
return Scaffold(
appBar: AppBar(
title: Text('Home'),
actions: [
Obx(() => authController.isLoggedIn.value
? IconButton(
icon: Icon(Icons.logout),
onPressed: () {
authController.logout();
},
)
: SizedBox.shrink()),
],
),
body: Center(
child: Obx(() => authController.isLoggedIn.value
? Text('Welcome!')
: ElevatedButton(
onPressed: () {
authController.login('user', 'password');
},
child: Text('Login'),
)),
),
);
}
}
ข้อผิดพลาดที่พบบ่อย: Dependency ไม่ถูก Inject
ข้อผิดพลาดที่พบบ่อยที่สุดคือการพยายามใช้ Get.find() เพื่อดึง dependency ที่ยังไม่ได้ถูก inject เข้าไปใน GetX container ก่อนหน้านี้ ในกรณีนี้ GetX จะ throw exception
วิธีแก้ไข: ตรวจสอบให้แน่ใจว่าคุณได้ใช้ Get.put() เพื่อ inject dependency เข้าไปใน GetX container ก่อนที่จะใช้ Get.find() เพื่อดึง dependency นั้น
ข้อผิดพลาดที่พบบ่อย: ลืม wrap Widget ด้วย Obx
หากคุณต้องการให้ Widget rebuild ตัวเองเมื่อ state เปลี่ยนแปลง อย่าลืม wrap Widget นั้นด้วย Obx() หากคุณลืม wrap Widget ด้วย Obx() Widget จะไม่ rebuild ตัวเองและจะไม่แสดงการเปลี่ยนแปลงของ state
วิธีแก้ไข: Wrap Widget ที่ต้องการ react ต่อการเปลี่ยนแปลงของ state ด้วย Obx()
สรุป
ในบทความนี้ เราได้เรียนรู้เกี่ยวกับการจัดการ Dependency ด้วย GetX ใน Flutter เราได้เรียนรู้เกี่ยวกับ Get.put(), Get.find() และ Obx ซึ่งเป็นเครื่องมือสำคัญในการสร้างแอปพลิเคชันที่สามารถบำรุงรักษาได้ง่ายและสามารถทดสอบได้ ในบทความถัดไป เราจะมาเรียนรู้เกี่ยวกับการใช้ BLoC Pattern สำหรับ State Management ซึ่งเป็นอีกหนึ่งแนวทางที่ได้รับความนิยมในการจัดการ state ใน Flutter