Flutter串接restful api

Flutter串接restful api


創建新專案

架構: 9e58a92b69d62fbea7da195f2657c470.png

package 1ee475de531d52b522cf5857249957db.png

使用JSONPlaceholder測試: JSONPlaceholder

使用quicktype將json檔案轉承我們要的形式: quicktype

測試使用posts:

1a9290a4d1078a54868fd2369e4cd6b0.png


建立Model

在models裡新增post_model:

import 'dart:convert';
 
class PostModel {
  PostModel({
    this.userId,
    this.id,
    this.title,
    this.body,
  });
 
  int userId;
  int id;
  String title;
  String body;
 
  factory PostModel.fromJson(String str) => PostModel.fromMap(json.decode(str));
 
  String toJson() => json.encode(toMap());
 
  factory PostModel.fromMap(Map<String, dynamic> json) => PostModel(
        userId: json["userId"] == null ? null : json["userId"],
        id: json["id"] == null ? null : json["id"],
        title: json["title"] == null ? null : json["title"],
        body: json["body"] == null ? null : json["body"],
      );
 
  Map<String, dynamic> toMap() => {
        "userId": userId == null ? null : userId,
        "id": id == null ? null : id,
        "title": title == null ? null : title,
        "body": body == null ? null : body,
      };
}

稍微修改(with null-safety):

import 'dart:convert';
 
class PostModel {
  PostModel({
    this.userId,
    this.id,
    this.title,
    this.body,
  });
 
  int? userId;
  int? id;
  String? title;
  String? body;
 
  factory PostModel.fromJson(String str) => PostModel.fromMap(json.decode(str));
 
  String toJson() => json.encode(toMap());
 
  factory PostModel.fromMap(Map<String, dynamic> json) => PostModel(
        userId: json["userId"] ?? json["userId"],
        id: json["id"] ?? json["id"],
        title: json["title"] ?? json["title"],
        body: json["body"] ?? json["body"],
      );
 
  Map<String, dynamic> toMap() => {
        "userId": userId ?? userId,
        "id": id ?? id,
        "title": title ?? title,
        "body": body ?? body,
      };
}

models.dart:

export './post_model.dart';

建立service

在services裡新增post_service:

import 'dart:convert';
import 'dart:developer';
 
import 'package:flutter_restful/core/models/models.dart';
import 'package:http/http.dart' as http;
 
class PostService {
  Future<PostModel?> findById(String id) async {
    PostModel post;
    try {
      var headers = {'content-type': 'application/json'};
      var url = Uri.parse('https://jsonplaceholder.typicode.com/posts/$id');
      final response = await http.get(url, headers: headers);
 
      if (response.statusCode == 200) {
        post = PostModel.fromJson(response.body);
        return post;
      } else {
        log('請求失敗');
      }
    } catch (err) {
      log('post fetch and set catch error', error: err);
    }
  }
 
  Future<List<PostModel>> findAll() async {
    try {
      var headers = {'content-type': 'application/json'};
      var url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
      final response = await http.get(url, headers: headers);
      final extractedData = json.decode(response.body);
      final List<PostModel> loadedposts = [];
 
      for (var data in extractedData) {
        loadedposts.add(PostModel(
          id: data['id'],
          userId: data['userId'],
          title: data['title'],
          body: data['body'],
        ));
      }
      return loadedposts;
    } catch (err) {
      log('posts fetch and set catch error', error: err);
      return [];
    }
  }
 
  Future<PostModel> create(PostModel post, [String token = '']) async {
    try {
      var headers = {
        'content-type': 'application/json',
        'Authorization': token,
      };
      var url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
      final response = await http.post(
        url,
        headers: headers,
        body: json.encode({
          'id': post.id,
          'userId': post.userId,
          'title': post.title,
          'body': post.body,
        }),
      );
      log(response.body);
      return PostModel.fromJson(response.body);
    } catch (err) {
      log('Add error, ', error: err);
      throw Exception(err);
    }
  }
 
  Future<PostModel> delete(String postId, [String token = '']) async {
    try {
      var headers = {
        'content-type': 'application/json',
        'Authorization': token,
      };
      var url = Uri.parse('https://jsonplaceholder.typicode.com/posts/$postId');
      final response = await http.delete(
        url,
        headers: headers,
      );
      log(response.body);
      if (response.statusCode == 200) {
        return PostModel.fromJson(response.body);
      } else {
        throw Exception('Failed to delete post');
      }
    } catch (err) {
      log('Delete error, ', error: err);
      throw Exception(err);
    }
  }
 
  Future<PostModel> edit(String postId, PostModel post,
      [String token = '']) async {
    try {
      var headers = {
        'content-type': 'application/json; charset=UTF-8',
        'Authorization': token,
      };
      var url = Uri.parse('https://jsonplaceholder.typicode.com/posts/$postId');
 
      final response = await http.put(
        url,
        headers: headers,
        body: json.encode({
          'id': post.id,
          'userId': post.userId,
          'title': post.title,
          'body': post.body,
        }),
        // body: post.toJson()
      );
      log(response.body);
      if (response.statusCode == 200) {
        return PostModel.fromJson(response.body);
      } else {
        throw Exception('Failed to edit post');
      }
    } catch (err) {
      log('Edit error, ', error: err);
      throw Exception(err);
    }
  }
}

services.dart:

export './post_service.dart';

UI

home_screen.dart:

import 'package:flutter/material.dart';
import 'package:flutter_restful/core/models/models.dart';
import 'package:flutter_restful/core/services/post_service.dart';
import 'package:flutter_restful/ui/post_detail_screen.dart';
 
class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);
 
  @override
  _HomeScreenState createState() => _HomeScreenState();
}
 
class _HomeScreenState extends State<HomeScreen> {
  final _formKey = GlobalKey<FormState>();
  final TextEditingController _titleController = TextEditingController();
  final TextEditingController _contentController = TextEditingController();
 
  PostService post = PostService();
  PostModel model = PostModel();
  List<PostModel> _posts = [];
 
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    post.findAll().then((value) {
      setState(() {
        _posts = value;
      });
    });
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        itemCount: _posts.length,
        itemBuilder: (context, index) {
          PostModel _post = _posts[index];
 
          return ListTile(
            title: Text(_post.title ?? ''),
            dense: true,
            onTap: () {
              Navigator.push(
                  context,
                  MaterialPageRoute(
                      builder: (context) => PostDetailScreen(
                            post: _post,
                          )));
            },
            trailing: Wrap(
              spacing: 12, // space between two icons
              children: <Widget>[
                IconButton(
                  onPressed: () {
                    post.delete(index.toString());
                    setState(() {
                      _posts.removeAt(index);
                    });
                  },
                  icon: const Icon(Icons.delete),
                ),
                IconButton(
                  onPressed: () {
                    showDialog(
                        context: context,
                        builder: (BuildContext context) {
                          return AlertDialog(
                            content: Stack(
                              clipBehavior: Clip.none,
                              children: <Widget>[
                                Positioned(
                                  right: -40.0,
                                  top: -40.0,
                                  child: InkResponse(
                                    onTap: () {
                                      Navigator.of(context).pop();
                                    },
                                    child: const CircleAvatar(
                                      child: Icon(Icons.close),
                                      backgroundColor: Colors.red,
                                    ),
                                  ),
                                ),
                                Form(
                                  key: _formKey,
                                  child: Column(
                                    mainAxisSize: MainAxisSize.min,
                                    children: <Widget>[
                                      const Text('Edit Post'),
                                      Padding(
                                        padding: const EdgeInsets.all(8.0),
                                        child: TextFormField(
                                          decoration: const InputDecoration(
                                            labelText: "title",
                                          ),
                                          initialValue: _posts[index].title,
                                          onSaved: (value) {
                                            model.title = value;
                                          },
                                        ),
                                      ),
                                      Padding(
                                        padding: const EdgeInsets.all(8.0),
                                        child: TextFormField(
                                          decoration: const InputDecoration(
                                            labelText: "content",
                                          ),
                                          initialValue: _posts[index].body,
                                          onSaved: (value) {
                                            model.body = value;
                                          },
                                        ),
                                      ),
                                      Padding(
                                        padding: const EdgeInsets.all(8.0),
                                        child: ElevatedButton(
                                          child: const Text("Edit"),
                                          onPressed: () {
                                            if (_formKey.currentState!
                                                .validate()) {
                                              _formKey.currentState!.save();
                                              model.userId = 1;
                                              model.id = _posts[index].id;
                                              post
                                                  .edit(
                                                      _posts[index]
                                                          .id
                                                          .toString(),
                                                      model)
                                                  .then(
                                                (value) {
                                                  setState(() {
                                                    _posts[index] = value;
                                                  });
                                                },
                                              );
                                              Navigator.of(context).pop();
                                            }
                                          },
                                        ),
                                      )
                                    ],
                                  ),
                                ),
                              ],
                            ),
                          );
                        });
                  },
                  icon: const Icon(Icons.edit),
                ),
              ],
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {
          showDialog(
              context: context,
              builder: (BuildContext context) {
                return AlertDialog(
                  content: Stack(
                    clipBehavior: Clip.none,
                    children: <Widget>[
                      Positioned(
                        right: -40.0,
                        top: -40.0,
                        child: InkResponse(
                          onTap: () {
                            Navigator.of(context).pop();
                          },
                          child: const CircleAvatar(
                            child: Icon(Icons.close),
                            backgroundColor: Colors.red,
                          ),
                        ),
                      ),
                      Form(
                        key: _formKey,
                        child: Column(
                          mainAxisSize: MainAxisSize.min,
                          children: <Widget>[
                            const Text('Add Post'),
                            Padding(
                              padding: const EdgeInsets.all(8.0),
                              child: TextFormField(
                                controller: _titleController,
                                decoration: const InputDecoration(
                                  labelText: "title",
                                ),
                                onSaved: (value) {
                                  model.title = value;
                                },
                              ),
                            ),
                            Padding(
                              padding: const EdgeInsets.all(8.0),
                              child: TextFormField(
                                controller: _contentController,
                                decoration: const InputDecoration(
                                  labelText: "content",
                                ),
                                onSaved: (value) {
                                  model.body = value;
                                },
                              ),
                            ),
                            Padding(
                              padding: const EdgeInsets.all(8.0),
                              child: ElevatedButton(
                                child: const Text("Add"),
                                onPressed: () {
                                  if (_formKey.currentState!.validate()) {
                                    _formKey.currentState!.save();
                                    model.userId = 1;
                                    post.create(model).then(
                                      (value) {
                                        setState(() {
                                          _posts.add(value);
                                        });
                                      },
                                    );
                                    Navigator.of(context).pop();
                                  }
                                },
                              ),
                            )
                          ],
                        ),
                      ),
                    ],
                  ),
                );
              });
        },
      ),
    );
  }
}

post_detail_screen.dart:

import 'package:flutter/material.dart';
import 'package:flutter_restful/core/models/models.dart';
 
class PostDetailScreen extends StatefulWidget {
  final PostModel post;
  const PostDetailScreen({
    Key? key,
    required this.post,
  }) : super(key: key);
 
  @override
  _PostDetailScreenState createState() => _PostDetailScreenState();
}
 
class _PostDetailScreenState extends State<PostDetailScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.post.title ?? ''),
      ),
      body: Column(
        children: [
          Text(widget.post.body ?? ''),
        ],
      ),
    );
  }
}
 

main.dart:

import 'package:flutter/material.dart';
 
import 'ui/home_screen.dart';
 
void main() {
  runApp(const MyApp());
}
 
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
 
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomeScreen(),
    );
  }
}

Test

app-test.gif