In this tutorial, we will set up a GraphQL endpoint in a Strapi backend along with Flutter, a powerful open-source UI development kit for Android, iOS, Linux, Mac, Windows, Google Fuchsia, and the web to build a Todo app.
Strapi is an open-source headless CMS based on Nodejs that lets developers design APIs fast and manage content efficiently. Some features it offers among others include:
Strapi is 100% open-source. It is hosted on GitHub with over 50K stars and a large community for support. Strapi also has a forum and Discord server where Strapi users can ask questions and get answers and discuss the newest features and releases of Strapi.
Strapi is highly customizable with feature-rich plugins. Recently they introduced workflows which enable you to easily create, visualize and manage review content stages hence improving the collaboration experience and giving you control over the entire content lifecycle. A complete list of features and enhancements is available in the Strapi changelog. Strapi also has a highly customizable Amin UI for building collections and APIs with a marketplace where developers can search, install and interact with plugins.
You need no server. Strapi comes bundled with its server. All we have to do is scaffold a Strapi project, run its server, and we are good to go. You don't need to write any server code. Strapi does all that.
Strapi hosts your Collection in RESTful and GraphQL endpoints, and these endpoints can be consumed by clients (Angular, Flutter, Desktop, cURL, etc.). With Strapi, you don't have to worry about server setup and coding. There will be no need to create models and controllers because Strapi has all that baked in and ready to use. From the Strapi admin UI, we can create our collections and single types. A collection maps to the endpoints:
/YOUR_COLLECTION_s
: Creates new content./YOUR_COLLECTION_s
: Gets all the contents./YOUR_COLLECTION_s/:ID
: Gets a single content based on its ID
./YOUR_COLLECTION_s/:ID
: Edits a content/YOUR_COLLECTION_s/:ID
: Deletes a content.We will be building a todo app in Flutter to demonstrate how we can communicate from a Flutter app to a Strapi backend to store, edit and delete our todo items.
To follow this tutorial, you need to have some binaries installed on your machine:
v4.3.9
and aboveYarn
: Very fast Node package manager. You can install via NPM: npm i yarn -g
.flutter CLI
: This command-line tool is used to manage a Flutter project. We can use it to create a Flutter project. Visit https://flutter.dev/docs/get-started/install to install the CLI for your machine.We will create the main folder where our Strapi project and Flutter project will reside.
mkdir todo-app
Move into the folder: cd todo-app
then create a Strapi project with the command below:
yarn create strapi-app todo-app-backend --quickstart
# OR
npx create-strapi-app todo-app-backend --quickstart
This command creates a Strapi project in todo-app
with necessary dependencies and starts the server by running yarn develop
.
The page http://localhost:1337/admin/auth/register/
will be opened in the browser for you to set up your Strapi administrator credentials.
Fill in your details and click on the "LET'S START" button. Strapi will create your account and will load the admin UI like below. From this page, we create our collections.
We are building a todo app so we will create a Todo model that looks exactly like the one below:
1 Todo {
2 name
3 done
4 }
The model above represents a todo item we will have in our app. The name
is the name or text of a todo, e.g., "Read 100 years of love." The done
is a boolean field that indicates whether a todo item has been done or not.
Now, let's create the collection.
On the admin UI, click on Content-Type Builder, then, click on the + Create new collection
type button. A modal will pop up; on the popup modal, type "todo" in the Display name
input box. The "todo" will be the name of our collection type.
Click on the "Continue"
button and on the following UI that appears on the exact modal, create the fields for the "todo" collection.
Select the "Text" field on the next display and type in "name."
Click on the "+ Add another field"
button, and on the next display, select "Boolean" and type in "done" on the next display that appears.
Click on the "Finish"
button, the modal will disappear, and we will see the "todo" collection on the page with the fields we just added.
Click on the "Save"
button on the top right. It will save our "todo"
collection. We will see that a "Todo"
is a content type on the sidebar menu of the dashboard.
We will add mock Todo data to our collection.
But before that, we will disable the Draft/Publish feature so that we can directly save and publish items easily(note that this is not recommended in a production environment.)
From the dashboard, we click on Content-type Builder -> Todo
and click on the edit icon
From the popup menu that appears we select the advanced advanced settings tab and uncheck the Draft&Publish
checkbox and click on Finish
Now click on Content Manager
on the left sidebar; under the todo collection type, click on the + Create new entry
button at the top-right of the page. A Create an entry
UI will appear.
You will see input boxes for all the fields in our Todo model. Add the data below:
1- `name` -> Read 100 years of love
2- `done` -> false
After adding them, save and go back to the todo page to add another entry
1- `name` -> Find a new home
2- `done` -> false
Feel free to add many items as you like.
Next, we will open access for all users, both unauthenticated and authenticated users.
Click on the Settings
item on the sidebar menu.
Go to the "USERS & PERMISSIONS PLUGIN"
section and click on "Roles," and then on Public
on the right section.
A Public
page is loaded in this section. Scroll down to the Todo
area under the Permission
section and check the Select all
box, and finally click on the Save
button on the top-right page, to make our endpoints accessible to the Public.
In the next step, we add GraphQL to our collection.
By default, Strapi provides our endpoints via REST, but here we want the endpoints to be accessed via GraphQL. To do that, we use the following command to install the GraphQL plugin for Strapi.
yarn strapi install graphql
Article updated by: Kevine Nzapdi
Strapi will install the dependency and rebuild the admin UI. Visiting http://localhost:1337/graphql in your browser, will load the GraphQL playground:
We can play with our GraphQL from the playground. On the playground, Strapi will create GraphQL mutations and queries for the todos
collection that looks like the one below.
1 input TodoInput {
2 name: String
3 done: Boolean
4 }
5
6 // Todo's type definition
7 type Todo {
8 name: String
9 done: Boolean
10 createdAt: DateTime
11 updatedAt: DateTime
12 }
13
14 type TodoEntity {
15 id: ID
16 attributes: Todo
17 }
18
19 type Query {
20 todo(id: ID): TodoEntityResponse
21 todos(
22 filters: TodoFiltersInput
23 pagination: PaginationArg = {}
24 sort: [String] = []
25 ): TodoEntityResponseCollection
26 }
Note: To find all the queries and mutations created for your collections, click on the “SCHEMA” item on the middle right-side of the GraphQL Playground. A right-side bar will appear listing the queries and mutations schema for your collections.
All the queries and mutations will be done via http://localhost:1337/graphql. Now let's test some queries and mutations in the GraphQL playground.
To retrieve all the todos in our collection, we run the query:
1# Write your query or mutation here
2query {
3 todos {
4 data {
5 id
6 attributes {
7 name
8 done
9 }
10 }
11 }
12}
To retrieve a single todo item from our collection we run the query:
1query {
2 todo(id: 1) {
3 data {
4 id
5 attributes {
6 name
7 done
8 }
9 }
10 }
11}
To create a new todo we run the below mutation:
1mutation createTodo {
2 createTodo(data: { name: "Find a new house", done: false }) {
3 data {
4 id
5 attributes {
6 name
7 done
8 }
9 }
10 }
11}
To update to todo item run the below mutation:
1mutation updateTodo {
2 updateTodo(id: "1", data: { done: true }) {
3 data {
4 id
5 attributes {
6 name
7 done
8 }
9 }
10 }
11}
To delete a todo run the mutation below:
1mutation deleteTodo {
2 deleteTodo(id: 22) {
3 data {
4 id
5 attributes {
6 name
7 done
8 }
9 }
10 }
11}
Now it’s time to build the Flutter app.
Make sure you have the Flutter and Dart SDK fully installed on your machine. If you are having issues with Flutter, run flutter doctor
to iron them out. Once everything is set up, run flutter --version
to make sure the Flutter CLI is available globally in your system.
Step out of the todo-app-backend
folder and run the command below:
flutter create todo_app_frontend
The command creates a Flutter project directory called todo_frontend
that contains a simple demo app that uses Material Components. So now we should have two folders(todo-app-backend
and todo_app_frontend
) in the todo-app
folder
Move into the todo_app_frontend
folder:
cd todo_app_frontend
Make sure your simulator/emulator is running. You can check if your emulator is running and active by running the command: flutter devices
.
Once everything is okay,we start the app with the command flutter run
.
We will see the app launched in our emulator. So the need step will consist of building the actual todo flutter app. We go back to the Flutter project. You will see a main.dart
file in the project. That is the main file in Flutter projects, and it is where the app is being bootstrapped from. Everything in Flutter is a widget.
Our app will have three widgets:
CreateTodo
: This widget is where we will create new todos.TodoList
: This widget will get the list of all the todos in our system.ViewTodo
: This widget is where we will view our todos, and edit and delete them.Our final app will look like this:
To build the app, we will use these dependencies:
graphql_flutter
: This is a GraphQL client for Flutter that gives us APIs to run queries and mutations conversationally.intl
: This library provides us with DateTime formatting capabilities.Open the pubspec.yaml
file, go to the dependencies
section and add graphql_flutter
and intl
.
1 dependencies:
2 flutter:
3 sdk: flutter
4 graphql_flutter: ^5.1.2
5 intl: ^0.18.1
Then run flutter pub get
in your terminal. Flutter will install the dependencies in your project.
Create a project structure like the screenshot below:
We have two folders in the lib directory. The first one API
hosts a file graphqlConfig.dart
that hold the GraphQL configurations while the screens
holds the different screens of our app.
We will flesh out the code in them. To connect to a GraphQL server, we will create a GraphQLClient
. This GraphQLClient
will contain a link and cache system.
According to comments on the GraphQLClient
source code: The link is a Link over which GraphQL documents will be resolved into a Response. The cache is the GraphQLCache to use for caching results and optimistic updates.
We will create a GraphQLConfiguration
class in the lib/api/graphQLConfig.dart
file, and this class will have a clientToQuery
method that will return an instance of GraphQLClient
.
Open lib/GraphQLConfig.dart
and paste the below code:
1 import "package:flutter/material.dart";
2 import "package:graphql_flutter/graphql_flutter.dart";
3 class GraphQLConfiguration {
4 static HttpLink httpLink = HttpLink(
5 'http://10.0.2.2:1337/graphql',
6 );
7 static ValueNotifier<GraphQLClient> client = ValueNotifier(
8 GraphQLClient(
9 cache: GraphQLCache(),
10 link: httpLink,
11 ),
12 );
13 static ValueNotifier<GraphQLClient> clientToQuery() {
14 return client;
15 }
16 }
17
18
19 static HttpLink httpLink = HttpLink(
20 'http://10.0.2.2:1337/graphql',
21 );
This particular block code above sets the link where the GraphQLClient
will resolve documents. You can notice that the link is http://10.0.2.2:1337/graphql
, instead of http://localhost:1337/graphql
. The emulator proxies HTTP requests made inside it. Since we are running the flutter app on an emulator, the proxy URL is 10.0.2.2
, and this URL will forward the HTTP request made to the URL to localhost
.
Our Strapi backend runs on localhost:1337
, we have to make an HTTP request to 10.0.2.2:1337. The emulator will proxy it to localhost:1337
.
The cache: GraphQLCache()
makes the GraphQLClient
use its internal cache. We create an instance of GraphQLClient
and store it in the client
. This is returned in the clientToQuery
static method.
Open the lib/main.dart
and paste the following code:
1 import 'dart:math';
2 import 'package:flutter/material.dart';
3 import 'package:graphql_flutter/graphql_flutter.dart';
4 import 'package:intl/intl.dart';
5 import 'package:todo_frontend/screens/createTodo.dart';
6 import 'package:todo_frontend/screens/viewTodo.dart';
7 import 'package:todo_frontend/api/graphqlConfig.dart';
8
9 void main() {
10 runApp(const MyApp());
11 }
12 class MyApp extends StatelessWidget {
13 const MyApp({super.key});
14 // This widget is the root of your application.
15
16 Widget build(BuildContext context) {
17 return GraphQLProvider(
18 client: GraphQLConfiguration.clientToQuery(),
19 child: MaterialApp(
20 title: 'Todo App',
21 theme: ThemeData(
22 colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
23 useMaterial3: true,
24 ),
25 home: const TodoList(),
26 ));
27 }
28 }
Note that we imported all the packages that we will be using.
The main
function is the entry point where the execution starts. The runApp
function starts rendering the widgets in our app. Notice that we passed it MyApp
widget. This widget is the first widget to render its UI in our todo app.
Each widget overrides the build
method from either StatelessWidget
or StatefulWidget
to return widgets that will render the UI of our app.
A StatelessWidget
manages no local state. It is just like a functional component in Reactjs without useState
. A StatefulWidget
manages a local state. It is like a functional component in Reactjs with the useState
hook.
The MyApp
extends the StatelesWidget
because it will be managing no state. In its build method, we have a context argument that is of the BuildContext
instance. BuildContext
is a handle for the location of a widget in the widget tree.
The GraphQLClient
has Mutation
and Query
widgets. These widgets give us options from where we can make queries and mutations to our GraphQL server. Before making these queries and mutations, we must wrap the Query
and Mutation
widgets in the GraphQLProvider widget.
That's why in the build
method of the MyApp
, we wrapped the MaterialApp
widget in GraphQLProvider
. As a result, the TodoList
widget can now access the Query
and Mutation
widgets.
This widget makes a query to fetch all the todos in our Strapi backend, which happens when the widgets load. Then, it will render the todos in a list. Each todo list will have an onTap
event registered on them so that when pressed, a ViewTodo
widget screen is opened to view the item.
Also, in this widget, we will have a FloatingActionButton
, when clicked will open the CreateTodo
widget screen for us to add new todos.
Paste the code below to build the TodoList
screen after the MyApp
widget in main.dart
.
1 class TodoList extends StatefulWidget {
2 const TodoList({Key? key}) : super(key: key);
3
4
5 TodoListState createState() => TodoListState();
6 }
7
8 class TodoListState extends State<TodoList> {
9 String readTodos = """
10 query {
11 todos(sort:"createdAt:desc") {
12 data {
13 id
14 attributes {
15 name
16 done
17 createdAt
18 }
19 }
20 }
21 } """;
22
23 var colors = [
24 Colors.indigo,
25 Colors.green,
26 Colors.purple,
27 Colors.pinkAccent,
28 Colors.red,
29 Colors.black
30 ];
31
32 Random random = Random();
33 List<Map<String, dynamic>> todos = [];
34
35 randomColors() {
36 int randomNumber = random.nextInt(colors.length);
37 return colors[randomNumber];
38 }
39
40 onChange(b) {
41 return true;
42 }
43
44
45 Widget build(BuildContext context) {
46 return Scaffold(
47 body: Query(
48 options: QueryOptions(
49 document: gql(readTodos),
50 pollInterval: const Duration(seconds: 0),
51 ),
52 builder: (QueryResult result,
53 {VoidCallback? refetch, FetchMore? fetchMore}) {
54 if (result.hasException) {
55 return Text(result.exception.toString());
56 }
57
58 if (result.isLoading) {
59 return const Center(child: Text('Loading'));
60 }
61
62 Map<String, dynamic> todos = result.data?["todos"];
63
64 return Scaffold(
65 backgroundColor: Colors.white,
66 body: Column(children: [
67 Container(
68 alignment: Alignment.centerLeft,
69 padding: const EdgeInsets.fromLTRB(8, 50, 0, 9),
70 color: Colors.blue,
71 child: const Text(
72 "Todo",
73 style: TextStyle(
74 fontSize: 45,
75 fontWeight: FontWeight.bold,
76 color: Colors.white),
77 )),
78 const SizedBox(
79 height: 10,
80 ),
81 Padding(
82 padding: const EdgeInsets.symmetric(vertical: 3),
83 child: ListView.builder(
84 itemCount: todos["data"].length,
85 shrinkWrap: true,
86 itemBuilder: (context, index) {
87 return GestureDetector(
88 onTap: () {
89 Navigator.push(
90 context,
91 MaterialPageRoute(
92 builder: (context) => ViewTodo(
93 id: todos["data"][index]["id"],
94 refresh: () {
95 refetch!();
96 }),
97 ),
98 );
99 },
100 child: Container(
101 margin: const EdgeInsets.fromLTRB(10, 0, 10, 10),
102 padding: const EdgeInsets.fromLTRB(10, 0, 10, 10),
103 decoration: BoxDecoration(
104 borderRadius:
105 const BorderRadius.all(Radius.circular(7)),
106 color: randomColors(),
107 ),
108 child: Row(
109 children: [
110 Expanded(
111 child: Column(
112 crossAxisAlignment:
113 CrossAxisAlignment.start,
114 children: [
115 Padding(
116 padding: const EdgeInsets.fromLTRB(
117 0, 6, 0, 6),
118 // child: Text('To do'),
119 child: Text(
120 todos['data'][index]["attributes"]
121 ["name"]
122 .toString(),
123 style: const TextStyle(
124 fontSize: 16,
125 color: Colors.white,
126 fontWeight: FontWeight.bold)),
127 ),
128 Text(
129 DateFormat("yMMMEd")
130 .format(DateTime.parse(todos['data']
131 [index]["attributes"]
132 ["createdAt"]
133 .toString()))
134 .toString(),
135 style: const TextStyle(
136 color: Colors.white),
137 ),
138 ],
139 ),
140 ),
141 Checkbox(
142 value: todos["data"][index]["attributes"]
143 ["done"],
144 onChanged: onChange,
145 checkColor: Colors.white,
146 activeColor: Colors.white,
147 )
148 ],
149 ),
150 ));
151 },
152 ),
153 ),
154 ]),
155 floatingActionButton: FloatingActionButton(
156 foregroundColor: Colors.white,
157 backgroundColor: Colors.green,
158 onPressed: () {
159 Navigator.push(
160 context,
161 MaterialPageRoute(
162 builder: (context) => CreateTodo(refresh: () {
163 refetch!();
164 }),
165 ),
166 );
167 },
168 tooltip: 'Add new todo',
169 child: const Icon(Icons.add),
170 ),
171 );
172 }),
173 );
174 }
175 }
The TodoList
uses the createState
method to create its mutatable state at the TodoListState
, and this TodoListState
renders the UI widget for the TodoList
.
Widgets that extend the State
class are:
1- The logic and internal state for a [StatefulWidget].
2- The State is information that
3- (1) can be read synchronously when the widget is built and
4- (2) might change during the widget's lifetime. It is the responsibility of the widget implementer to ensure that the [State] is promptly notified when such state changes, using [State.setState].
Inside the TodoListState
widget, we start by defining the query to read the todos in the readTodos
String variable. We have an array of colors, and we used this to change the background color of our todos list widget randomly.
The todos
variables will hold the todos list fetched from our backend. The randomColors
is the method that will randomly return a background color for each todo widget.
Inside the build method, see that the Query
widget wraps the whole widget tree. This is done to reference the returned todos. We also have a vital function refetch
that we can use to refresh our todos list when a change occurs.
This Query
widget uses the document
method in its options
object to query for the todos list. It does this by calling the gql
method with the readTodos variable. The result of this query is returned in the builder
function's result
argument.
Inside the function, we retrieve the result and assign it to the todos
collection:
1 Map<String, dynamic> todos = result.data?["todos"];
Then we return our UI starting from the Scaffold(...)
widget. We use todos[``"``data``"``]
to render each result there in the ListView.builder
.
We then use GestureDetector
widget to which we is set an onTap
event.
1child: ListView.builder(
2 itemCount: todos["data"].length,
3 shrinkWrap: true,
4 itemBuilder: (context, index) {
5 return GestureDetector(
6 onTap: () {
7 Navigator.push(
8 context,
9 MaterialPageRoute(
10 builder: (context) => ViewTodo(
11 id: todos["data"][index]["id"],
12 refresh: () {
13 refetch!();
14 }),
15 ),
16 );
17 },
18 child: Container(
19 margin: const EdgeInsets.fromLTRB(10, 0, 10, 10),
20 padding: const EdgeInsets.fromLTRB(10, 0, 10, 10),
21 decoration: BoxDecoration(
22 borderRadius:
23 const BorderRadius.all(Radius.circular(7)),
24 color: randomColors(),
25 ),
26 child: Row(
27 children: [
28 Expanded(
29 child: Column(
30 crossAxisAlignment:
31 CrossAxisAlignment.start,
32 children: [
33 Padding(
34 padding: const EdgeInsets.fromLTRB(
35 0, 6, 0, 6),
36 // child: Text('To do'),
37 child: Text(
38 todos['data'][index]["attributes"]
39 ["name"]
40 .toString(),
41 style: const TextStyle(
42 fontSize: 16,
43 color: Colors.white,
44 fontWeight: FontWeight.bold)),
45 ),
46 Text(
47 DateFormat("yMMMEd")
48 .format(DateTime.parse(todos['data']
49 [index]["attributes"]
50 ["createdAt"]
51 .toString()))
52 .toString(),
53 style: const TextStyle(
54 color: Colors.white),
55 ),
56 ],
57 ),
58 ),
59 Checkbox(
60 value: todos["data"][index]["attributes"]
61 ["done"],
62 onChanged: onChange,
63 checkColor: Colors.white,
64 activeColor: Colors.white,
65 )
66 ],
67 ),
68 ));
69 },
70),
From the code you can notice that when a Todo item in the list is pressed or tapped, the ViewTodo
widget screen is launched. We passed to it the id of the Todo and a refresh function. This refresh function calls the refetch
function returned by the Query
widget. This is done to refresh the TodoList
view from the ViewTodo
widget when a change to the Todo occur.
The FloatingActionButton
:
1 floatingActionButton: FloatingActionButton(
2 foregroundColor: Colors.white,
3 backgroundColor: Colors.green,
4 onPressed: () {
5 Navigator.push(
6 context,
7 MaterialPageRoute(
8 builder: (context) => CreateTodo(refresh: () {
9 refetch!();
10 }),
11 ),
12 );
13 },
14 tooltip: 'Add new todo',
15 child: const Icon(Icons.add),
16 ),
It launches the CreateTodo
widget on clicked. Let's look at the ViewTodo
widget.
We will perform three actions on a Todo in this widget.
To build this screen paste the code below in lib/screens/ViewTodo.dart
:
1 import 'package:flutter/material.dart';
2 import 'package:graphql_flutter/graphql_flutter.dart';
3 import 'package:todo_frontend/api/graphqlConfig.dart';
4 String readTodo = """
5 query(\$id: ID!) {
6 todo(id: \$id) {
7 data {
8 id
9 attributes {
10 name
11 done
12 }
13 }
14 }
15 }
16 """;
17 String updateTodo = """
18 mutation updateTodo(\$id: ID!, \$done: Boolean, \$name: String) {
19 updateTodo(id: \$id, data: { done: \$done, name: \$name}) {
20 data {
21 id
22 attributes {
23 name
24 done
25 }
26 }
27 }
28 }
29 """;
30 String deleteTodo = """
31 mutation deleteTodo(\$id: ID!) {
32 deleteTodo(id: \$id) {
33 data {
34 id
35 attributes {
36 name
37 done
38 }
39 }
40 }
41 }
42 """;
43 class ViewTodo extends StatefulWidget {
44 final id;
45 final refresh;
46 const ViewTodo({Key? key, this.id, this.refresh})
47 : super(key: key);
48
49 ViewTodoState createState() =>
50 ViewTodoState(id: this.id, refresh: this.refresh);
51 }
52 class ViewTodoState extends State<ViewTodo> {
53 late final id;
54 late final refresh;
55 ViewTodoState({Key? key, this.id, this.refresh});
56 var editMode = false;
57 var myController;
58 bool? done;
59
60 Widget build(BuildContext context) {
61 return GraphQLProvider(
62 client: GraphQLConfiguration.clientToQuery(),
63 child: Query(
64 options: QueryOptions(
65 document: gql(readTodo),
66 variables: {'id': id},
67 pollInterval: const Duration(seconds: 0)),
68 builder: (QueryResult result,
69 {VoidCallback? refetch, FetchMore? fetchMore}) {
70 if (result.hasException) {
71 return Text(result.exception.toString());
72 }
73 if (result.isLoading) {
74 return const Scaffold(body: Center(child: Text('Loading')));
75 }
76 // it can be either Map or List
77 var todo = result.data?["todo"];
78 done = todo\["data"\]["attributes"]["done"];
79 myController = TextEditingController(
80 text: todo\["data"\]["attributes"]["name"].toString());
81 return Scaffold(
82 appBar: AppBar(
83 elevation: 0,
84 automaticallyImplyLeading: false,
85 backgroundColor: Colors.blue,
86 flexibleSpace: SafeArea(
87 child: Container(
88 padding: const EdgeInsets.only(
89 right: 16, top: 4, bottom: 4, left: 0),
90 child: Row(children: <Widget>[
91 IconButton(
92 onPressed: () {
93 Navigator.pop(context);
94 },
95 icon: const Icon(
96 Icons.arrow_back,
97 color: Colors.white,
98 ),
99 ),
100 const SizedBox(
101 width: 20,
102 ),
103 const Text(
104 "View Todo",
105 style: TextStyle(
106 fontSize: 25,
107 fontWeight: FontWeight.bold,
108 color: Colors.white),
109 ),
110 ])))),
111 body: Container(
112 padding: const EdgeInsets.all(12),
113 margin: const EdgeInsets.all(8),
114 decoration: BoxDecoration(
115 borderRadius: BorderRadius.circular(9),
116 color: Colors.blue),
117 width: double.infinity,
118 height: 200,
119 child: editMode
120 ? Column(
121 children: [
122 Container(
123 width: double.infinity,
124 padding:
125 const EdgeInsets.fromLTRB(0, 0, 0, 4),
126 child: const Text("Todo:",
127 textAlign: TextAlign.left,
128 style: TextStyle(
129 color: Colors.white,
130 fontSize: 20,
131 ))),
132 TextField(
133 controller: myController,
134 decoration: const InputDecoration(
135 border: OutlineInputBorder(
136 borderSide: BorderSide(
137 width: 1, color: Colors.white)),
138 hintText: 'Add todo'),
139 ),
140 Row(
141 crossAxisAlignment: CrossAxisAlignment.center,
142 children: [
143 Container(
144 padding: const EdgeInsets.fromLTRB(
145 0, 0, 0, 4),
146 child: const Text("Done:",
147 textAlign: TextAlign.left,
148 style: TextStyle(
149 color: Colors.white,
150 fontSize: 20,
151 ))),
152 StatefulBuilder(builder:
153 (BuildContext context,
154 StateSetter setState) {
155 return Checkbox(
156 value: done,
157 onChanged: (value) {
158 setState(() {
159 done = value;
160 });
161 },
162 );
163 }),
164 ])
165 ],
166 )
167 : Column(
168 crossAxisAlignment: CrossAxisAlignment.center,
169 children: [
170 Row(
171 children: [
172 Container(
173 padding:
174 const EdgeInsets.fromLTRB(0, 0, 0, 4),
175 child: const Text("Todo:",
176 textAlign: TextAlign.left,
177 style: TextStyle(
178 color: Colors.white,
179 fontSize: 20,
180 )),
181 ),
182 const SizedBox(
183 width: 15,
184 ),
185 Container(
186 padding:
187 const EdgeInsets.fromLTRB(0, 0, 0, 4),
188 child: Text(
189 todo\["data"\]["attributes"]["name"]
190 .toString(),
191 textAlign: TextAlign.left,
192 style: const TextStyle(
193 color: Colors.white,
194 fontSize: 20,
195 fontWeight: FontWeight.bold)))
196 ],
197 ),
198 const SizedBox(
199 height: 10,
200 ),
201 Row(
202 children: [
203 const Text("Done:",
204 textAlign: TextAlign.left,
205 style: TextStyle(
206 color: Colors.white,
207 fontSize: 20,
208 )),
209 const SizedBox(
210 width: 15,
211 ),
212 Text(
213 todo\["data"\]["attributes"]["done"]
214 .toString(),
215 textAlign: TextAlign.left,
216 style: const TextStyle(
217 color: Colors.white,
218 fontSize: 20,
219 fontWeight: FontWeight.bold))
220 ],
221 ),
222 ],
223 ),
224 ),
225 floatingActionButton: !editMode
226 ? Mutation(
227 options: MutationOptions(
228 document: gql(deleteTodo),
229 update:
230 (GraphQLDataProxy cache, QueryResult? result) {
231 return cache;
232 },
233 onCompleted: (dynamic resultData) {
234 refresh();
235 ScaffoldMessenger.of(context).showSnackBar(
236 const SnackBar(content: Text('Done.')));
237 Navigator.pop(context);
238 },
239 ),
240 builder:
241 (RunMutation runMutation, QueryResult? result) {
242 return Column(
243 crossAxisAlignment: CrossAxisAlignment.end,
244 mainAxisAlignment: MainAxisAlignment.end,
245 children: [
246 FloatingActionButton(
247 backgroundColor: Colors.red,
248 foregroundColor: Colors.white,
249 heroTag: null,
250 child: const Icon(Icons.delete),
251 onPressed: () {
252 runMutation({'id': id});
253 ScaffoldMessenger.of(context)
254 .showSnackBar(const SnackBar(
255 content:
256 Text('Deleting todo...')));
257 },
258 ),
259 const SizedBox(
260 height: 10,
261 ),
262 FloatingActionButton(
263 foregroundColor: Colors.white,
264 backgroundColor: Colors.blue,
265 onPressed: () {
266 setState(() {
267 editMode = true;
268 });
269 },
270 tooltip: 'Edit todo',
271 child: const Icon(Icons.edit),
272 )
273 ]);
274 })
275 : Mutation(
276 options: MutationOptions(
277 document: gql(updateTodo),
278 update:
279 (GraphQLDataProxy cache, QueryResult? result) {
280 return cache;
281 },
282 onCompleted: (dynamic resultData) {
283 refresh();
284 refetch!();
285 ScaffoldMessenger.of(context).showSnackBar(
286 const SnackBar(content: Text('Done.')));
287 },
288 ),
289 builder: (
290 RunMutation runMutation,
291 QueryResult? result,
292 ) {
293 return Column(
294 crossAxisAlignment: CrossAxisAlignment.end,
295 mainAxisAlignment: MainAxisAlignment.end,
296 children: [
297 Padding(
298 padding:
299 const EdgeInsets.fromLTRB(0, 0, 0, 5),
300 child: FloatingActionButton(
301 foregroundColor: Colors.white,
302 backgroundColor: Colors.red,
303 heroTag: null,
304 child: const Icon(Icons.cancel),
305 onPressed: () {
306 setState(() {
307 editMode = false;
308 });
309 },
310 )),
311 FloatingActionButton(
312 foregroundColor: Colors.white,
313 backgroundColor: Colors.green,
314 heroTag: null,
315 child: const Icon(Icons.save),
316 onPressed: () {
317 ScaffoldMessenger.of(context)
318 .showSnackBar(const SnackBar(
319 content:
320 Text('Updating todo...')));
321 runMutation({
322 'id': id,
323 'name': myController.text,
324 'done': done
325 });
326 setState(() {
327 editMode = false;
328 });
329 },
330 )
331 ]);
332 }));
333 }));
334 }
335 }
We have three string variables set readTodo
, updateTodo
, and deleteTodo
.
readTodo
is a query data to return a todo by its id.updateTodo
is a mutation to update a todo using its id
with new done
and name
values.deleteTodo
is also a mutation that deletes a todo.Notice that the ViewTodo
is a stateful widget and manages its State in the ViewTodoState
widget. Every variable inside the ViewTodoState
widget is a state variable that can be updated during the widget's lifetime.
The constructor is set to accept the Todo's id and a refresh function. In the ViewTodoState
widget, see that we have an editMode
boolean variable. This variable sets the edit mode of the widget. We did this to toggle text fields we can use to edit this widget without the need for another widget screen.
The myController
is a text controller for a text field when editing the Todo in an edit mode. We use it to get the value typed in a TextField.
The bool? done;
is used to hold the done
field of the todo.
In the build
method, we enclosed the whole widget in the tree with the Query
widget. It calls the readTodo
on start-up and renders the name
and done
fields of the Todo in the UI.
We used a ternary operator to check when the editMode
is active and render text field and the checkbox to edit the Todo. If there is no edit mode, the todo details are rendered on Text widgets. Also, we are using the editMode
to render FloatingActionButtons
based on the current model.
If there is an edit mode, the save
and cancel
FloatingActionButtons
will show. The save
FloatingActionButton
will save the edited Todo. It will collect the name
value from TextField and collect the done
value from the State of the CheckBox. Then, it will call the runMutation
with the values.
There is an onCompleted
function of the Mutation
object enclosing the edit section of the save
and cancel
FloatingActionButton
.
1 onCompleted: (dynamic resultData) {
2 refresh();
3 refetch!();
4 ScaffoldMessenger.of(context).showSnackBar(
5 const SnackBar(content: Text('Done.')));
6 },
We call the refresh
method to refresh the list of todos in the TodoList
and the refetch
method from the Query
widget to refresh this ViewTodo
widget because the current Todo has been modified.
If there is no edit mode, the edit
and delete
FlatActionButton are shown. The edit
FlatActionButton, when clicked, sets the editMode
State to true
. The delete
FlatActionButton, when clicked, sends the deleteTodo
to delete the current Todo.
In the onCompleted
function of the Mutation
widget that enclosed it, we called the refetch
method and popped the ViewTodo
widget off the screen because it was deleted and is no longer available.
1 onCompleted: (dynamic resultData) {
2 refresh();
3 ScaffoldMessenger.of(context).showSnackBar(
4 const SnackBar(content: Text('Done.')));
5 Navigator.pop(context);
6 },
Let's code the CreateTodo
screen.
This method is where we create new todos. This screen will have a TextField where we can type in the name of the Todo to create. It will have a MaterialButton
that will run a mutation when clicked.
Paste the below code to lib/screens/createTodo.dart
:
1 import 'package:flutter/material.dart';
2 import 'package:graphql_flutter/graphql_flutter.dart';
3 import '../api/graphQLConfig.dart';
4 String addTodo = """
5 mutation createTodo(\$name: String, \$done: Boolean) {
6 createTodo(data: { name: \$name, done: \$done}) {
7 data {
8 id
9 attributes {
10 name
11 done
12 }
13 }
14 }
15 }
16 """;
17 class CreateTodo extends StatelessWidget {
18 final myController = TextEditingController();
19 final refresh;
20 CreateTodo({Key? key, this.refresh}) : super(key: key);
21
22 Widget build(BuildContext context) {
23 return GraphQLProvider(
24 client: GraphQLConfiguration.clientToQuery(),
25 child: Mutation(
26 options: MutationOptions(
27 document: gql(addTodo),
28 update: (GraphQLDataProxy cache, QueryResult? result) {
29 return cache;
30 },
31 onCompleted: (dynamic resultData) {
32 refresh!();
33 ScaffoldMessenger.of(context).showSnackBar(
34 const SnackBar(content: Text('New todo added.')));
35 Navigator.pop(context);
36 },
37 ),
38 builder: (
39 RunMutation runMutation,
40 QueryResult? result,
41 ) {
42 return Scaffold(
43 appBar: AppBar(
44 title: const Text("Create Todo"),
45 ),
46 body: Column(children: [
47 Container(
48 alignment: Alignment.centerLeft,
49 padding: const EdgeInsets.fromLTRB(10, 50, 10, 9),
50 child: TextField(
51 controller: myController,
52 decoration: const InputDecoration(
53 border: OutlineInputBorder(),
54 hintText: 'Add todo'),
55 )),
56 Row(children: [
57 Expanded(
58 child: Padding(
59 padding: const EdgeInsets.all(10),
60 child: MaterialButton(
61 onPressed: () {
62 runMutation({
63 'name': myController.text,
64 'done': false
65 });
66
67 ScaffoldMessenger.of(context).showSnackBar(
68 const SnackBar(
69 content: Text('Adding new todo...')));
70 },
71 color: Colors.blue,
72 padding: const EdgeInsets.all(17),
73 child: const Text(
74 "Add",
75 style: TextStyle(
76 fontWeight: FontWeight.bold,
77 color: Colors.white,
78 fontSize: 20),
79 ),
80 )))
81 ])
82 ]));
83 }));;
84 }
85 }
Notice that we have a createTodo
mutation set. This mutation string will create a new todo in our Strapi.
The CreateTodo
is a stateless widget, and it manages no state. The constructor accepts the refresh function passed to it and stores in it the refresh
variable.
myController
is a TextEditingController
used to manipulate TextFields. We wrap its widget tree in GraphQLProvider
and Mutation
widgets. The document
function will run the gql(createTodo)
function call when the runMutation
argument in its builder
function is called.
In the UI, a TextField is rendered. This is where the new todo name is typed. The myController
is set to the TextField. This will enable us to use the myController
to get the value of the TextField.
The MaterialButton
has an onPressed
event registered to it. Its handler will be called when the button is pressed. This will retrieve the value in the TextField using the myController
.
It will call the runMutation
function passing in the value in the TextField. This will run the createTodo
mutation creating a new todo in our Strapi backend.
The onCompleted
function will be called when the mutation completes:
1 onCompleted: (dynamic resultData) {
2 refresh!();
3 ScaffoldMessenger.of(context).showSnackBar(
4 const SnackBar(content: Text('New todo added.')));
5 Navigator.pop(context);
6 },
The refresh
function passed to the CreateTodo
widget from the TodoList
widget is called, so the todos list in the TodoList
widget is updated to display our newly added todo item.
Now let’s test our application to ensure that everything is working. If you run the application, you should be able to add, update, edit and delete a Todo as in the demo below
From this tutorial, we learn how to create a Strapi project, build collections using its admin dashboard and set up Graphql endpoints.
We created a simple Todo app in Flutter to show how we can consume the Strapi GraphQL endpoints from a mobile app.
Strapi is straightforward, to begin with, and provides an easy-to-understand doc. It can connect with any client, mobile, web, or desktop. Feel free to check out other quick guides Strapi Integrations to get started.
You can find the full working code of the Flutter application this To-Do App with Strapi GraphQL Plugin and Flutter repository