【入門】FlutterとElixir/PhoenixのGraphQLでTODOアプリを作る(Flutter編)


はじめに

こちらはクライアント側であるFlutterとGraphQLを使ってTODOアプリを作成します。

バックエンド側であるElixir/Phoenixの実装についてはこちらに書きました。


完成品

todo.gif


開発環境

Flutter 0.11.13

Dart 2.1.0


プロジェクト作成

~$ flutter create todo_app


プログラム作成


モデルの作成

TODOのタスク情報のモデルを作成します。scoped modelを使った状態管理をしています。と言っても単純なアプリなので使う必要もないと思いますが勉強がてら使いました。もっと良い使い方があればご教示願います。


lib/models/task.dart

import 'package:scoped_model/scoped_model.dart';

class Task extends Model {
int id;
String title;
String description;
int status;
List<Task> _myTasks = [];

Task({this.id, this.title, this.description, this.status});

factory Task.fromJson(Map<String, dynamic> json) {
return Task(
id: json['id'] is String ? int.parse(json['id']) : json['id'],
title: json['title'],
description: json['description'],
status: json['status'] is String ? int.parse(json['status']) : json['status']
);
}

List<Task> getMyTaskList() {
return this._myTasks;
}

Task getMyTask(int idx) {
return this._myTasks[idx];
}

void setMyTaskList(List<Task> taskList) {
this._myTasks = taskList;
notifyListeners();
}

void addMyTaskList(Task task) {
this._myTasks.add(task);
notifyListeners();
}

void deleteMyTask(int idx) {
this._myTasks.removeAt(idx);
notifyListeners();
}

}


ここではタスクリストを定義して、それを状態として持っています。状態の変更は関数を通じて行います。できることはタスクを作ること、一覧を取ること、削除できることです。


メインプログラムの作成

今回は特にGraphQLのクライアントを使わずに作成していきます。


lib/main.dart

import 'package:flutter/material.dart';

import 'package:http/http.dart' as http;
import 'dart:convert';
import './models/task.dart';
import 'package:scoped_model/scoped_model.dart';

void main() => runApp(MyApp());

Future<List<Task>> getTasks() async {
var response = await http.post('http://localhost:4000/graphql',
body: "query { allTasks{id title description status} }");
if (response.statusCode == 200) {
var responseJson = json.decode(response.body);
List data = responseJson['data']['allTasks'];
return data.map((e) => Task.fromJson(e)).toList();
} else {
throw Exception('Failed to get task list');
}
}

Future<Task> postTask(title, description) async {
var response = await http.post('http://localhost:4000/graphql',
body:
'mutation { createTask(title: "$title", description: "$description"){id title description status} }');
if (response.statusCode == 200) {
var responseJson = json.decode(response.body);
Task task = Task.fromJson(responseJson['data']['createTask']);
return task;
} else {
throw Exception('Failed to create task');
}
}

Future<bool> deleteTask(int id) async {
var response = await http.post('http://localhost:4000/graphql',
body: 'mutation { deleteTask(id: $id){id} }');
if (response.statusCode == 200) {
return true;
} else {
throw Exception('Failed to delete task');
}
}

class MyApp extends StatelessWidget {
// This widget is the root of your application.

_init(Task model) async {
model.setMyTaskList(await getTasks());
}

@override
Widget build(BuildContext context) {
Task model = Task();
this._init(model);

return MaterialApp(
title: 'TODO APP',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ScopedModel(model: model, child: MyHomePage()),
);
}
}

class MyHomePage extends StatelessWidget {
List<Task> todoList;
TextEditingController titleCtrl = TextEditingController();
TextEditingController descriptionCtrl = TextEditingController();

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('タスク一覧'),
),
body: ScopedModelDescendant(
builder: (BuildContext context, Widget child, Task model) {
return Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: this.titleCtrl,
decoration:
InputDecoration(hintText: '例:買い物', labelText: 'タイトル'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: this.descriptionCtrl,
decoration:
InputDecoration(hintText: '例:じゃがいもを買う', labelText: '内容')),
),
FlatButton(
child: Icon(Icons.add),
onPressed: () {
postTask(this.titleCtrl.text, this.descriptionCtrl.text)
.then((task) {
this.titleCtrl.clear();
this.descriptionCtrl.clear();
model.addMyTaskList(task);
});
},
color: Colors.blue,
textColor: Colors.white,
),
Flexible(
child: model.getMyTaskList().length == 0
? Center(child: Text('タスクはありません'))
: ListView.builder(
itemCount: model.getMyTaskList().length,
itemBuilder: (BuildContext context, int idx) {
return ListTile(
title: Text(model.getMyTask(idx).title),
subtitle: Text(model.getMyTask(idx).description),
onLongPress: () {
deleteTask(model.getMyTask(idx).id).then((val){
if(val) {
model.deleteMyTask(idx);
final snackBar = SnackBar(content: Text('Delete Success!'));
Scaffold.of(context).showSnackBar(snackBar);
}
});
},
);
})),
],
);
}),
);
}
}


状態を伝搬させたい場合はScopedModelで囲んで伝搬させます。

home: ScopedModel(model: model, child: MyHomePage()),

伝搬されたモデルを使うにはScopedModelDescendantを使います。

ScopedModelDescendantで囲んだところはmodelを自由に操作できます。

基本的にはGraphQLのAPIを叩いた後に状態を変化させる関数を呼び出して変更しています。

APIの実行はclassの外側に定義しています。

Future<List<Task>> getTasks() async {

var response = await http.post('http://localhost:4000/graphql',
body: "query { allTasks{id title description status} }");
if (response.statusCode == 200) {
var responseJson = json.decode(response.body);
List data = responseJson['data']['allTasks'];
return data.map((e) => Task.fromJson(e)).toList();
} else {
throw Exception('Failed to get task list');
}
}

Future<Task> postTask(title, description) async {
var response = await http.post('http://localhost:4000/graphql',
body:
'mutation { createTask(title: "$title", description: "$description"){id title description status} }');
if (response.statusCode == 200) {
var responseJson = json.decode(response.body);
Task task = Task.fromJson(responseJson['data']['createTask']);
return task;
} else {
throw Exception('Failed to create task');
}
}

Future<bool> deleteTask(int id) async {
var response = await http.post('http://localhost:4000/graphql',
body: 'mutation { deleteTask(id: $id){id} }');
if (response.statusCode == 200) {
return true;
} else {
throw Exception('Failed to delete task');
}
}

bodyに文字列を渡してあげることで実行できます。リストを取るときはqueryを使い、情報操作する場合はmutationを使います。この辺はElixir編のGraphiQLで自由にクエリーを投げて遊べるのでぜひやってみてください。


おわりに

今回はFlutter側には特にGraphQL関連のクライアントライブラリは入れませんでした。

調べてみると下記のようなライブラリーがあるようです。

graphql-flutter

興味がある方はこちらで実装してみるのもありかもしれません。