Flutter: Call Restful API (Provider+Retrofit)
Published: 2022-09-05 13:48:43

## สร้าง Project

เริ่มต้นสร้าง Flutter project ชื่อ tripbooking (project ตัวอย่าง) กันก่อนเลย โดยการรันคำสั่งสร้าง project และเปิด vscode ที่ folder นั้น

```
flutter create tripbooking
cd tripbooking
code .
```
สร้าง folder ที่จะเอาไว้เก็บไฟล์ต่างๆให้เป็นระเบียบ โดยในที่นี่ขึ้นอยู่กับความเหมาะสม สำหรับที่จะแนะนำให้สร้าง folder ทั้งหมด 3 folder คือ

- page = เอาไว้เก็บหน้าต่างๆ ของ application
- service = ส่วนประมวลผลเรียก Api
- model = สำหรับเก็บ database model

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4sazty0tlkvbacdug26d.png)

เพิ่ม libraries ที่เกี่ยวข้องกับ project

```
flutter pub add retrofit
flutter pub add retrofit_generator
flutter pub add json_serializable
flutter pub add build_runner --dev
flutter pub add dio
flutter pub add provider
flutter pub add get
flutter pub add get_storage
```

---

## สร้าง Provider ไว้จัดการข้อมูลที่ใช้ใน Application

- สร้าง Folder ย่อยชื่อ provider ภายใน folder service 
- สร้างไฟล์ของ provider ชื่อ appdata.dart

ในไฟล์ appdata.dart สร้างตัวแปล baseUrl ของ api ที่ application เราต้องติดต่อ ตามตัวอย่าง

```dart
import 'package:flutter/material.dart';

class AppData with ChangeNotifier {
  //Api baseurl
  String baseurl = "http://192.168.1.100:8888";
}

```

ไปที่ main.dart แก้ Code ใน main() เพื่อใช้งาน provider ที่สร้างขึ้น

```dart
void main() {
  runApp(MultiProvider(providers: [
    ChangeNotifierProvider(
      create: (context) => AppData(),
    )
  ], child: const MyApp()));
}
```

เรียบร้อยเรามี provider ไว้ใช้งานใน application เราแล้ว

---

## สร้าง RestClient โดยใช้ Retrofit 

ตัวเรียก api ในที่นี้เราจะเรียกว่า service เป็นไฟล์ที่เก็บใน folder service ซึ่งเราจะแยกตาม path (url) ของ restful api เช่น จะเรียกใช้งาน api ที่มี path เป็น /destination เราก็จะสร้างไฟล์ชื่อ destination.dart

สร้างไฟล์ destination.dart ใน folder service

```dart
import 'package:retrofit/retrofit.dart';
import 'package:dio/dio.dart';
import 'package:tripbooking/model/destination.dart';

part 'destination.g.dart';

@RestApi()
abstract class DestinationService {
  factory DestinationService(Dio dio, {String baseUrl}) = _DestinationService;

}

```

**ซึ่งเมื่อพิมพ์คำสั่งเสร็จแล้วมันจะ Error ซึ่งถูกแล้ว** เพราะว่า Retrofit ต้องมีการ generate คำสั่งใหม่ของมัน (https://pub.dev/packages/retrofit) เป็นไฟล์ชื่อ destination.g.dart ตามที่เราระบุ 

เปิด terminal แล้วพิมพ์คำสั่งให้ retrofit generate ไฟล์

```
flutter pub run build_runner watch
```

มันจะ generate ไฟล์แล้วเปิดค้างรอ ถ้าเราแก้ไขไฟล์มันก็จะ auto generate ใหม่ ถ้าจะยกเลิก ก็กด Ctrl+C หรือเราสามารถ ใช้คำสั่ง flutter pub run build_runner build (ใช้ option build) ก็ได้ มันจะ generate ครั้งเดียว ถ้าแก้ไฟล์ ต้องมาสั่ง generate ใหม่เอง

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/11g6xjxcjwqxywd9npq6.png)

ไฟล์ที่ generate ขึ้นเราไม่แก้ไขมัน เพราะมันจะถูกลบแล้วถูกสร้างใหม่ตลอด

ตอนนี้จะมี Retrofit client (service) ที่ใช้ติดต่อไปยัง api แล้ว ต่อไปจะเป็นการสร้าง method ในการเรียก api แต่จะเรียก api ได้ ต้องมี model มารับข้อมูลที่ได้คืนมาจาก api ด้วยด้วย ดังนั้น ต้องสร้าง model ก่อน

- สร้างไฟล์ destination.dart ใน folder model
- เข้าเวบ https://app.quicktype.io แล้วก็เอา json ที่ได้จาก api มาวางเพื่อให้เวบมันสร้าง class ของ dart ให้ตามรูปตัวอย่าง

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9lyqx236yz1hqqu4tvts.png)

1. ตั้งชื่อ class เป็น Destination (เพราะเป็นข้อมูลที่ได้จากการเรียก api destination)
2. วาง json ได้มา ลงไป
3. เลือกภาษาเป็น Dart
4. เลือกตัวเลือกแค่ตัวเดียวคือ Make all properties required
5. กด copy code

กลับไปที่ไฟล์ destination.dart ใน model แล้ววาง code ลงไป แต่ใน flutter จะ Error เพราะ @required ให้ลง @ ออก

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6frpeern4a97fqsuyy2z.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0avu06e4zvichl9kst2v.png)

ตอนนี้ เราก็จะมี model ไว้รับข้อมูลจาก api แล้ว

กลับไปที่ไฟล์ destination.dart ใน folder service (retrofit) แล้วเขียน คำสั่งเพิ่มเพื่อสร้าง method ในการเรียก api

```dart
abstract class DestinationService {
  factory DestinationService(Dio dio, {String baseUrl}) = _DestinationService;

  @GET("/destination")
  Future<List<Destination>> getDestinations();
}
```

จะเป็นการเรียกไปยัง path /destination และจะได้ผลลัพธ์ออกมาเป็น List ของ Destination (mode ที่เราเพิ่งสร้าง) นั่นเอง เมื่อ save ตัว retrofit จะ generate ไฟล์ให้ (ถ้าเรายังสั่งเป็น watch อยู่) หรือสั่ง build เพื่อ generate ใหม่อีกครั้ง

เรียบร้อย เราสร้างตัว service ที่เอาไว้เรียก api ได้แล้ว

อ่านเพิ่มเติมรูปแบบการใช้ retrofit ที่ (https://pub.dev/packages/retrofit)

---

## เรียกใช้งาน service ที่เราสร้าง

ตัวอย่างเช่น เราสร้างหน้า home.dart ใน folder page ในรูปแบบ FutureBuilder

```dart
import 'dart:convert';
import 'dart:developer';

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:tripbooking/model/destination.dart';
import 'package:tripbooking/service/destination.dart';
import 'package:provider/provider.dart';
import 'package:tripbooking/service/provider/appdata.dart';

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  // 1. กำหนดตัวแปร
  List<Destination> destinations = [];
  late Future<void> loadDataMethod;
  late DestinationService destinationService;

  // 2. สร้าง initState เพื่อสร้าง object ของ service 
  // และ async method ที่จะใช้กับ FutureBuilder
  @override
  void initState() {
    super.initState();
  // 2.1 object ของ service โดยต้องส่ง baseUrl (จาก provider) เข้าไปด้วย
    destinationService = 
        DestinationService(Dio(), baseUrl: context.read<AppData>().baseurl); 
  // 2.2 async method
    loadDataMethod = loadData(); 
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home Page'),
      ),
  // 3. เรียก service ในรูปแบบของ FutureBuilder (หรือจะไม่ใช้ก็ได้ แค่ตัวอย่างให้ดูเฉยๆ)
      body: FutureBuilder(
          future: loadDataMethod, // 3.1 object ของ async method
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.done) {
              return Center(child: Text(jsonEncode(destinations)));
            } else {
              return const Center(child: CircularProgressIndicator());
            }
          }),
    );
  }
  // 4. Async method ที่เรียก service เมื่อได้ข้อมูลก็เอาไปเก็บไว้ที่ destinations ที่ประกาศไว้ด้านบนเป็น List
  Future<void> loadData() async {
    try {
      destinations = await destinationService.getDestinations();
    } catch (err) {
      log('Error: $err');
    }
  }
}

```

เท่านี้ เราก็เรียกใช้ service ได้อย่างง่ายๆ 

**แต่ปัญหาคือต้องฝึกดูเวลามันผิดพลาด เกิด Error ตอนเรียก service มันจะต้องใช้ try catch จับ ตามตัวอย่าง**