Bouw een To-Do-app met Strapi GraphQL-plug-in en Flutter

In dit artikel wordt een GraphQL-eindpunt opgezet in een Strapi-backend samen met Flutter, een krachtige open-source UI-ontwikkelingskit voor Android, iOS, Linux, Mac, Windows, Google Fuchsia en het web om een ​​Todo-app te bouwen.

Wat is Strapi?

Strapi is een open-source headless CMS op basis van Nodejs waarmee ontwikkelaars snel API's kunnen ontwerpen en inhoud efficiënt kunnen beheren.

De kenmerken van Strapi omvatten:

Open source
Strapi is 100% open source. Het wordt gehost op Github https://github.com/strapi/strapi met meer dan 36K sterren en een grote community voor ondersteuning. Strapi heeft ook een forum waar Strapi-gebruikers vragen kunnen stellen en antwoorden kunnen krijgen en de nieuwste functies en releases van Strapi kunnen bespreken.

Aanpasbaar
Strapi is in hoge mate aanpasbaar met functierijke plug-ins. Onlangs hebben ze de i18n-plug-in geïntroduceerd om meertalige ondersteuning in uw inhoud mogelijk te maken. De Strapi UI-beheerder is geweldig voor het bouwen van collecties en API's. Er is een marktplaats waar ontwikkelaars plug-ins kunnen zoeken en installeren in hun Strapi-projecten.

Zelf gehost
Je hebt geen server nodig. Strapi wordt geleverd met zijn server. Het enige wat we hoeven te doen is een Strapi-project op te stellen, de server te runnen en we zijn klaar om te gaan. U hoeft geen servercode te schrijven. Dat doet Strapi allemaal.

RESTful of GraphQL
Strapi host uw verzameling in RESTful- en GraphQL-eindpunten, en deze eindpunten kunnen worden gebruikt door clients (Angular, Flutter, Desktop, cURL, enz.).

Met Strapi hoeft u zich geen zorgen te maken over serverconfiguratie en codering. Het is niet nodig om modellen en controllers te maken, want Strapi heeft dat allemaal ingebakken en klaar voor gebruik. Vanuit de gebruikersinterface van Strapi admin kunnen we onze collecties en afzonderlijke typen maken. Een verzameling verwijst naar de eindpunten:

  • POST /YOUR_COLLECTION_s :Creëert nieuwe inhoud.
  • KRIJGEN /YOUR_COLLECTION_s :Krijgt alle inhoud.
  • KRIJGEN /YOUR_COLLECTION_s/:ID :Krijgt een enkele inhoud op basis van zijn ID .
  • PUT /YOUR_COLLECTION_s/:ID :Bewerkt een inhoud
  • VERWIJDEREN /YOUR_COLLECTION_s/:ID :verwijdert een inhoud.

We gaan een todo-app bouwen in Flutter om te laten zien hoe we kunnen communiceren van een Flutter-app naar een Strapi-backend om onze todo-items op te slaan, te bewerken en te verwijderen.

Vereisten
Om deze tutorial te volgen, moet je een aantal binaire bestanden op je computer hebben geïnstalleerd:

  • Nodejs en NPM:u moet de binaire bestanden van Node.js op onze computer hebben geïnstalleerd. Je kunt het downloaden vanaf hier Node.js downloadpagina.
  • Yarn :Zeer snelle Node-pakketbeheerder. U kunt installeren via NPM:npm i garen -g.
  • flutter CLI :Deze opdrachtregeltool wordt gebruikt om een ​​Flutter-project te beheren. We kunnen het gebruiken om een ​​Flutter-project te maken. Ga naar https://flutter.dev/docs/get-started/install om de CLI voor uw machine te installeren.# Scaffold a Strapi

We zullen de hoofdmap maken waar ons Strapi-project en Flutter-project zich zullen bevinden.

mkdir strapi_flutter

Ga naar de map:cd strapi_flutter . We beginnen met het maken van een Strapi-project, voer het onderstaande commando uit:

yarn create strapi-app todo-api --quickstart
# OR
npx create-strapi-app todo-api --quickstart

Deze opdracht maakt een Strapi-project in todo-API met de nodige afhankelijkheden en start de server door yarn develop . uit te voeren .

De pagina http://localhost:1337/admin/auth/register/ wordt in de browser geopend zodat u uw Strapi-beheerdersreferenties kunt instellen.

Vul uw gegevens in en klik op de knop "LET'S START". Strapi maakt uw account aan en laadt de beheerdersinterface. Vanaf deze pagina maken we onze collecties.

Bouw de todo-verzameling

We bouwen een Todo-app, dus we zullen een Todo-model maken dat er precies zo uitziet als hieronder:

Todo {
    name
    done
}

Het bovenstaande model vertegenwoordigt een todo-item dat we in onze app zullen hebben. De name is de naam of tekst van een taak, bijvoorbeeld 'Koop boodschappen in de winkel'. De done is een Booleaans veld dat aangeeft of een taak is uitgevoerd of niet.

Laten we nu de verzameling maken.

Klik in de beheerdersinterface op Content-Type Builder en klik vervolgens op de + Create new collection typ knop. Er verschijnt een modaal; typ in de pop-upmodus "todo" in de Display name invoerveld. De "todo" is de naam van ons verzamelingstype.

Klik op de "Continue" knop en op de volgende gebruikersinterface die op de exacte modal verschijnt. In deze gebruikersinterface beginnen we met het maken van de velden voor de "todo"-verzameling.

  • Selecteer het veld "Tekst" op het volgende scherm en typ "name."
  • Klik op de "+ Add another field" en selecteer in het volgende scherm "Boolean" en typ "klaar" in het volgende scherm dat verschijnt.
  • Klik op de "Finish" knop, verdwijnt de modal en zien we de "todo"-verzameling op de pagina met de velden die we zojuist hebben toegevoegd.

Klik op de "Save" knop rechtsboven. Het zal onze "todo" . opslaan verzameling. We zullen zien dat een "Todos" is een inhoudstype in het zijbalkmenu van het dashboard.

Nepgegevens voor zaadjes

We zullen nep Todo-gegevens aan onze verzameling toevoegen.

Klik op de "Todos" in de linkerzijbalk; op de pagina die wordt geladen, ziet u een tabel zonder gegevens. Klik vervolgens op de + Add New Todos knop in de rechterbovenhoek van de pagina. Een Create an entry UI zal verschijnen. U ziet invoervakken voor alle velden in ons Todo-model.

Voeg de onderstaande gegevens toe:

- `name` -> Go to groceries store
- `done` -> false

Nadat je ze hebt toegevoegd, klik je op de Save en klik op de Publish knop die nu actief is. Deze knop maakt onze nieuwe gegevens live.

Ga terug naar onze "Todos" pagina's. U ziet de nieuwe gegevens in de tabel. Laten we een tweede gegevens toevoegen. Klik op de + Add New Todos en voeg op de volgende pagina die verschijnt de onderstaande gegevens toe:

- `name` -> Go to church
- `done` -> false

Klik op Save en dan op Publish . Vervolgens openen we de toegang voor alle gebruikers, zowel niet-geverifieerde als geverifieerde gebruikers.

Open toegang

Klik op de Settings item in het zijbalkmenu, ga dan op de pagina die in het rechtergedeelte wordt geladen naar de "USERS & PERMISSIONS PLUGIN" sectie en klik op "Rollen" en vervolgens op Public aan de rechterkant.

Een Public pagina wordt in deze sectie geladen. Scrol vervolgens omlaag naar de Permission sectie en controleer de Select all doos. Klik vervolgens op de Save knop op de pagina rechtsboven, en dit maakt onze eindpunten nu toegankelijk voor het publiek.

Vervolgens voegen we GraphQL toe aan onze collectie.

Strapi GraphQL-plug-in

Standaard levert Strapi onze eindpunten via REST, maar hier willen we dat de eindpunten toegankelijk zijn via GraphQL. Om dat te doen, installeren we de GraphQL-plug-in voor Strapi.

We zullen het installeren via de opdrachtregel, dus voer de onderstaande opdracht uit:

yarn strapi install graphql

Strapi zal de afhankelijkheid installeren en de beheerdersinterface opnieuw opbouwen. Ga nu naar http://localhost:1337/graphql in uw browser, hierdoor wordt de GraphQL-speeltuin geladen:

We kunnen spelen met onze GraphQL vanuit de speeltuin. Op de speelplaats zal strapi GraphQL-mutaties en queries maken voor de todos collectie die eruitziet zoals hieronder.

// Todo's type definition
type Todo {
  id: ID!
  created_at: DateTime!
  updated_at: DateTime!
  name: String
  done: Boolean
  published_at: DateTime
}

type TodoConnection {
  values: [Todo]
  groupBy: TodoGroupBy
  aggregate: TodoAggregator
}

type Query {
  // gets a single todo via id
  todo(id: ID!, publicationState: PublicationState): Todo

  // Gets all todos
  todos(
    sort: String
    limit: Int
    start: Int
    where: JSON
    publicationState: PublicationState
  ): [Todo]

  // This gives us more leverage on what to return in the query. E.g, it provides an aggregator that we can use to get the total count of todos data in the backend.
  todosConnection(
    sort: String
    limit: Int
    start: Int
    where: JSON
  ): TodoConnection
}

type Mutation {
  // creates a new todo
  createTodo(input: createTodoInput): createTodoPayload

  // Updates a todo
  updateTodo(input: updateTodoInput): updateTodoPayload

  // deletes a todo
  deleteTodo(input: deleteTodoInput): deleteTodoPayload
}

Opmerking :Om alle query's en mutaties te vinden die voor uw collecties zijn gemaakt, klikt u op het item "SCHEMA" rechts in het midden van de GraphQL-speeltuin. Er verschijnt een rechterzijbalk met het query- en mutatieschema voor uw collecties.

Alle zoekopdrachten en mutaties zullen worden gedaan via http://localhost:1337/graphql. Laten we onze zoekopdrachten en mutaties testen in de GraphQL-speeltuin.

Alle taken ophalen
Om alle taken in onze verzameling op te halen, voeren we de query uit:

# Write your query or mutation here
query {
  todos {
    name
    done
  }
}

Een taak opvragen
Om een ​​enkel todo-item uit onze collectie op te halen, voeren we de volgende zoekopdracht uit:

# Write your query or mutation here
query {
  todo(id: 1) {
    name
    done
  }
}

Maak een nieuwe taak
Om een ​​nieuwe taak aan te maken, voeren we de onderstaande mutatie uit:

# Write your query or mutation here
mutation {
  createTodo(input: { data: { name: "Clean the house", done: false } }) {
    todo {
      name
      done
    }
  }
}

Een taak bijwerken
Voer de onderstaande mutatie uit om bij te werken naar een todo-item:

# Write your query or mutation here
mutation {
  updateTodo(input: { where: { id: 3 }, data: { done: true } }) {
    todo {
      name
      done
    }
  }
}

Een taak verwijderen
Voer de onderstaande mutatie uit om een ​​taak te verwijderen:

# Write your query or mutation here
mutation {
  deleteTodo(input: { where: { id: 3 } }) {
    todo {
      name
      done
    }
  }
}

Nu bouwen we de Flutter-app.

Fladderen

Zorg ervoor dat de Flutter en Dart SDK volledig in uw machine is geïnstalleerd. Als je problemen hebt met Flutter, voer dan flutter doctor . uit om ze glad te strijken. Nadat alles is gedaan, voert u flutter --version uit om ervoor te zorgen dat de Flutter CLI wereldwijd beschikbaar is in uw systeem.

Fladder instellen

Dus hebben we een Flutter-project opgezet. Zorg ervoor dat u de todo-api . verlaat map. Vanuit de centrale map strapi_flutter voer het onderstaande commando uit:

flutter create todo_strapi

De opdracht maakt een Flutter-projectdirectory aan met de naam todo_strapi die een eenvoudige demo-app bevat die materiaalcomponenten gebruikt.

Ga naar de map:

cd todo_strapi

Zorg ervoor dat uw simulator/emulator actief is. U kunt controleren of uw emulator actief en actief is door het commando uit te voeren:flutter devices .

Nu starten we de app, voer het commando flutter run uit .

Bouw de Flutter-app

We zullen zien dat de app in onze emulator wordt gelanceerd. Nu gaan we terug naar het Flutter-project. Je ziet een main.dart bestand in het project. Dat is het hoofdbestand in Flutter-projecten en het is waar de app wordt opgestart. Alles in Flutter is een widget.

Onze app heeft drie widgets:

  • CreateTodo :In deze widget zullen we nieuwe taken maken.
  • TodoList :Deze widget krijgt de lijst met alle taken in ons systeem.
  • ViewTodo :In deze widget zullen we onze taken bekijken, bewerken en verwijderen.

Onze uiteindelijke app ziet er als volgt uit:



We zullen dus enkele afhankelijkheden gebruiken:

  • graphql_flutter :Dit is een GraphQL-client voor Flutter die ons API's geeft om query's en mutaties in een gesprek uit te voeren.
  • intl :Deze bibliotheek biedt ons DateTime-opmaakmogelijkheden.

Open de pubspec.yaml bestand, ga naar de dependencies sectie en voeg graphql_flutter . toe en intl .

dependencies:
  flutter:
    sdk: flutter
  intl:
  graphql_flutter: ^4.0.0-beta

Voer flutter pub get uit in uw terminal. Flutter zal de afhankelijkheden in uw project installeren.
Maak nu de bestanden:

mkdir lib/screens
touch lib/screens/ViewTodo.dart lib/screens/CreateTodo.dart lib/GraphQLConfig.dart

We zullen de code erin uitwerken. Om verbinding te maken met een GraphQL-server, maken we een GraphQLClient . Deze GraphQLClient zal een link- en cachesysteem bevatten.

Volgens opmerkingen over de GraphQLClient broncode:de link is een link waarover GraphQL-documenten worden omgezet in een [Response]. De cache is de [GraphQLCache] om te gebruiken voor cacheresultaten en optimistische updates.

We maken een GraphQLConfiguration klasse in de GraphQLConfig.dart bestand, en deze klasse heeft een clientToQuery methode die een instantie van GraphQLClient . retourneert .

GraphQLConfig.dart
Open lib/GraphQLConfig.dart en plak de onderstaande code:

import "package:flutter/material.dart";
import "package:graphql_flutter/graphql_flutter.dart";

class GraphQLConfiguration {
    static HttpLink httpLink = HttpLink(
      'http://10.0.2.2:1337/graphql',
    );

    static ValueNotifier<GraphQLClient> client = ValueNotifier(
      GraphQLClient(
        cache: GraphQLCache(),
        link: httpLink,
      ),
    );

   static ValueNotifier<GraphQLClient> clientToQuery() {
    return client;
  }
}

De code

    static HttpLink httpLink = HttpLink(
      'http://10.0.2.2:1337/graphql',
    );

De bovenstaande code stelt de link in waar de GraphQLClient zal documenten oplossen. Zie dat de link http://10.0.2.2:1337/graphql . is , maar waarom is dat? Onze Strapi-backend draait op http://localhost:1337/graphql , niet op http://10.0.2.2:1337/graphql .
Het antwoord is dat we de Flutter-app op een emulator gebruiken.

Emulator proxy's HTTP-verzoeken die erin worden gedaan. De proxy-URL is 10.0.2.2 , en deze URL stuurt het HTTP-verzoek door naar de URL naar localhost . Daarom gebruiken we de localhost . niet URL.

Aangezien onze Strapi-backend draait op localhost:1337 , dan moeten we een HTTP-verzoek doen naar 10.0.2.2:1337. De emulator zal het proxyen naar localhost:1337 .
De cache: GraphQLCache() maakt de GraphQLClient gebruik de interne cache.
We maken een instantie van GraphQLClient en slaat het op in de client . Dit wordt geretourneerd in de clientToQuery statische methode.

main.dart
Open de lib/main.dart en plak de onderstaande code:

import 'package:flutter/material.dart';
import 'dart:math';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:intl/intl.dart';
import 'GraphQLConfig.dart';
import 'screens/CreateTodo.dart';

import 'screens/ViewTodo.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  runApp(MyApp());
}

class MyApp extends StatelessWidget {

  const MyApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GraphQLProvider(
        client: GraphQLConfiguration.clientToQuery(),
        child: MaterialApp(
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: TodoList(),
        ));
  }
}

We hebben de pakketten geïmporteerd die we nodig hebben.

De main functie is het startpunt van Flutter-apps. Dit startpunt is waar de uitvoering begint. Vervolgens de runApp begint met het renderen van de widgets in onze app. Zie dat we geslaagd zijn voor MyApp widget. Deze widget is de eerste widget die de gebruikersinterface in onze app weergeeft.

Elke widget overschrijft de build methode van ofwel StatelessWidget of StatefulWidget om widgets terug te geven die de gebruikersinterface van onze app weergeven.

Een StatelessWidget beheert geen lokale staat. Het is net als een functionele component in Reactjs zonder useState .
Een StatefulWidget bestuurt een lokale staat. Het is als een functionele component in Reactjs met de useState haak.

De MyApp breidt de StatelesWidget uit omdat het geen staat zal besturen. In de bouwmethode hebben we een contextargument dat van de BuildContext . is voorbeeld. BuildContext is een handvat naar de locatie van een widget in de widgetstructuur.

De GraphQLClient heeft Mutation en Query widgets. Deze widgets geven ons opties van waaruit we queries en mutaties kunnen maken naar onze GraphQL-server. Voordat we deze query's en mutaties maken, moeten we de Query en Mutation widgets in de GraphQLProvider-widget.

Daarom in de build methode van de MyApp , we hebben de MaterialApp . ingepakt widget in GraphQLProvider . Als gevolg hiervan is de TodoList widget heeft nu toegang tot de Query en Mutation widgets.

TodoList
Deze widget voert een query uit om alle taken in onze Strapi-backend op te halen, wat gebeurt wanneer de widgets worden geladen. Vervolgens zal het de taken in een lijst weergeven. Elke takenlijst heeft een onTap gebeurtenis die erop is geregistreerd, zodat wanneer erop wordt gedrukt een ViewTodo widgetscherm wordt geopend om het ingedrukte taakitem te bekijken.

In deze widget hebben we ook een FloatingActionButton dat, wanneer erop wordt geklikt, het CreateTodo-widgetscherm wordt geopend zodat we nieuwe taken kunnen toevoegen. Deze TodoList zal een stateful widget zijn. Plak de onderstaande code onder de MyApp widget in main.dart .

...

class TodoList extends StatefulWidget {
  TodoList({Key key}) : super(key: key);

  @override
  _TodoListState createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
  String readTodos = """
    query {
      todos(sort:"created_at:desc") {
        id
        name
        done
        created_at
      }
    }
  """;

  var colors = [
    Colors.amber,
    Colors.green,
    Colors.purple,
    Colors.orange,
    Colors.red,
    Colors.yellow
  ];
  Random random = new Random();
  var todos = [];

  randomColors() {
    int randomNumber = random.nextInt(colors.length);
    return colors[randomNumber];
  }

  onChanged(b) {
    return true;
  }

  @override
  Widget build(BuildContext context) {

    return Query(
        options: QueryOptions(
          document: gql(readTodos),
          pollInterval: Duration(seconds: 0),
        ),
        builder: (QueryResult result,
            {VoidCallback refetch, FetchMore fetchMore}) {
          if (result.hasException) {
            return Text(result.exception.toString());
          }

          if (result.isLoading) {
            return Text('Loading');
          }

          todos = result.data["todos"];

          return Scaffold(
            body: Column(children: [
              Container(
                  alignment: Alignment.centerLeft,
                  padding: const EdgeInsets.fromLTRB(8, 50, 0, 9),
                  color: Colors.blue,
                  child: Text(
                    "Todo",
                    style: TextStyle(
                        fontSize: 45,
                        fontWeight: FontWeight.bold,
                        color: Colors.white),
                  )),
              Expanded(
                  child: ListView.builder(
                itemCount: todos.length,
                shrinkWrap: true,
                itemBuilder: (context, index) {
                  return GestureDetector(
                      onTap: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                            builder: (context) => ViewTodo(
                              id: todos\[index\]["id"],
                              refresh: () {
                                refetch();
                              },
                            ),
                          ),
                        );
                      },
                      child: Container(
                        margin: const EdgeInsets.fromLTRB(10, 0, 10, 10),
                        padding: const EdgeInsets.fromLTRB(10, 0, 10, 10),
                        decoration: BoxDecoration(
                          borderRadius: BorderRadius.all(Radius.circular(7)),
                          color: randomColors(),
                        ),
                        child: Row(
                          children: [
                            Expanded(
                              child: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: [
                                  Padding(
                                    padding:
                                        const EdgeInsets.fromLTRB(0, 6, 0, 6),
                                    child: Text(
                                        todos\[index\]["name"]
                                            .toString() /*"Go to the grocery store"*/,
                                        style: TextStyle(
                                            fontSize: 25,
                                            fontWeight: FontWeight.bold)),
                                  ),
                                  Text(DateFormat("yMMMEd")
                                      .format(DateTime.parse(todos[index]
                                              ["created_at"]
                                          .toString()))
                                      .toString()),
                                ],
                              ),
                            ),
                            Checkbox(
                                value: todos\[index\]["done"],
                                onChanged: onChanged)
                          ],
                        ),
                      ));
                },
              ))
            ]),
            floatingActionButton: FloatingActionButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => CreateTodo(refresh: () {
                      refetch();
                    }),
                  ),
                );
              },
              tooltip: 'Add new todo',
              child: Icon(Icons.add),
            ),
          );
        });
  }
}

De TodoList gebruikt de createState methode om zijn veranderlijke staat te creëren op de _TodoListState , en deze _TodoListState rendert de UI-widget voor de TodoList .

Widgets die de State . uitbreiden klasse zijn:

- The logic and internal state for a [StatefulWidget].
- The State is information that (1) can be read synchronously when the widget is built and (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].

In de _TodoListState widget, beginnen we met het definiëren van de query om de taken in de readTodos . te lezen Tekenreeksvariabele. We hebben een reeks kleuren en we hebben deze gebruikt om de achtergrond van onze takenlijst-widget willekeurig te kleuren.

De todos variabelen bevatten de takenlijst die is opgehaald uit onze backend. De randomColors is de methode die willekeurig een kleur retourneert voor elke actiewidget.

Zie in de bouwmethode dat de Query widget wikkelt de hele widgetboom. Dit wordt gedaan om te verwijzen naar de geretourneerde taken en een vitale functie refetch we kunnen gebruiken om onze takenlijst te vernieuwen wanneer er een wijziging optreedt.

Deze Query widget gebruikt de document methode in zijn options object om op te vragen voor de takenlijst. Het doet dit door de gql . te bellen methode met de readTodos variabele. Het resultaat van deze zoekopdracht wordt geretourneerd in de builder functie's result argument.

Binnen de functie halen we het resultaat op en wijzen het toe aan de todos variabele:

todos = result.data["todos"];

Vervolgens retourneren we onze gebruikersinterface vanaf de Scaffold(...) widget. We gebruiken de todos variabele om elk resultaat daar in de ListView.builder . weer te geven , waarmee het resultaat wordt opgebouwd in een lijst met de aangepaste gebruikersinterface die we hebben ingesteld.

De GestureDetector widget is ingesteld op elke takenlijst om een ​​onTap . te plaatsen gebeurtenis op hen.

                  child: ListView.builder(
                itemCount: todos.length,
                shrinkWrap: true,
                itemBuilder: (context, index) {
                  return GestureDetector(
                      onTap: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                            builder: (context) => ViewTodo(
                              id: todos\[index\]["id"],
                              refresh: () {
                                refetch();
                              },
                            ),
                          ),
                        );
                      },
...

Zie dat wanneer een Todo-item in de lijst wordt ingedrukt of getikt, de ViewTodo widgetscherm wordt gestart. We hebben het de id van de Todo en een vernieuwingsfunctie doorgegeven. Deze verversingsfunctie roept de refetch . aan functie geretourneerd door de Query widget. Dit wordt gedaan om de TodoList . te vernieuwen uitzicht vanaf de ViewTodo widget wanneer er een wijziging in de Todo is gemaakt.

De FloatingActionButton :

...
floatingActionButton: FloatingActionButton(
  onPressed: () {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => CreateTodo(refresh: () {
          refetch();
        }),
      ),
    );
  },
  tooltip: 'Add new todo',
  child: Icon(Icons.add),
),
...

Het lanceert de CreateTodo widget wanneer erop wordt geklikt. Laten we eens kijken naar de ViewTodo widget.

ViewTodo
We zullen drie acties uitvoeren op een Todo in deze widget. We zullen een taak opvragen met behulp van de id die aan deze widget is doorgegeven, we zullen een taak bijwerken en we zullen de taak verwijderen.

Plak de onderstaande code in lib/screens/ViewTodo.dart :

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

import '../GraphQLConfig.dart';

String readTodo = """
  query(\$id: ID!) {
  todo(id: \$id) {
    name
    done
  }
}
""";

String updateTodo = """
mutation(\$id: ID!, \$done: Boolean, \$name: String) {
  updateTodo(input: { where: { id: \$id }, data: { done: \$done, name: \$name } }) {
    todo {
      name
      done
    }
  }
}
""";

String deleteTodo = """
mutation(\$id: ID!) {
  deleteTodo(input: { where: { id: \$id } }) {
    todo {
      name
      done
    }
  }
}
""";

class ViewTodo extends StatefulWidget {
  final id;
  final refresh;
  ViewTodo({Key key, @required this.id, this.refresh}) : super(key: key);

  @override
  ViewTodoState createState() => ViewTodoState(id: id, refresh: this.refresh);
}

class ViewTodoState extends State<ViewTodo> {
  final id;
  final refresh;
  ViewTodoState({Key key, @required this.id, this.refresh});

  var editMode = false;
  var myController;
  bool done;

  @override
  Widget build(BuildContext context) {
    return GraphQLProvider(
        client: GraphQLConfiguration.clientToQuery(),
        child: Query(
            options: QueryOptions(
              document: gql(readTodo),
              variables: {'id': id},
              pollInterval: Duration(seconds: 0),
            ),
            builder: (QueryResult result,
                {VoidCallback refetch, FetchMore fetchMore}) {
              if (result.hasException) {
                return Text(result.exception.toString());
              }

              if (result.isLoading) {
                return Text('Loading');
              }

              // it can be either Map or List
              var todo = result.data["todo"];
              done = todo["done"];
              myController =
                  TextEditingController(text: todo["name"].toString());

              return Scaffold(
                appBar: AppBar(
                    elevation: 0,
                    automaticallyImplyLeading: false,
                    backgroundColor: Colors.blue,
                    flexibleSpace: SafeArea(
                        child: Container(
                            padding: EdgeInsets.only(
                                right: 16, top: 4, bottom: 4, left: 0),
                            child: Row(children: <Widget>[
                              IconButton(
                                onPressed: () {
                                  Navigator.pop(context);
                                },
                                icon: Icon(
                                  Icons.arrow_back,
                                  color: Colors.white,
                                ),
                              ),
                              SizedBox(
                                width: 20,
                              ),
                              Text(
                                "View Todo",
                                style: TextStyle(
                                    fontSize: 25,
                                    fontWeight: FontWeight.bold,
                                    color: Colors.white),
                              ),
                            ])))),
                body: Container(
                  padding: const EdgeInsets.all(12),
                  margin: const EdgeInsets.all(8),
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(9),
                  ),
                  width: double.infinity,
                  child: editMode
                      ? Column(
                          children: [
                            Container(
                                width: double.infinity,
                                padding: const EdgeInsets.fromLTRB(0, 0, 0, 4),
                                child: Text("Todo:",
                                    textAlign: TextAlign.left,
                                    style: TextStyle(
                                      color: Colors.black,
                                      fontSize: 20,
                                    ))),
                            TextField(
                              controller: myController,
                              decoration: InputDecoration(
                                  border: OutlineInputBorder(),
                                  hintText: 'Add todo'),
                            ),
                            Row(
                                crossAxisAlignment: CrossAxisAlignment.center,
                                children: [
                                  Container(
                                      padding:
                                          const EdgeInsets.fromLTRB(0, 0, 0, 4),
                                      child: Text("Done:",
                                          textAlign: TextAlign.left,
                                          style: TextStyle(
                                            color: Colors.black,
                                            fontSize: 20,
                                          ))),
                                  StatefulBuilder(builder:
                                      (BuildContext context,
                                          StateSetter setState) {
                                    return new Checkbox(
                                      value: done,
                                      onChanged: (bool value) {
                                        print("done:" + done.toString());
                                        setState(() {
                                          done = value;
                                        });
                                      },
                                    );
                                  }),
                                ])
                          ],
                        )
                      : Column(
                          children: [
                            Container(
                              width: double.infinity,
                              padding: const EdgeInsets.fromLTRB(0, 0, 0, 4),
                              child: Text("Todo:",
                                  textAlign: TextAlign.left,
                                  style: TextStyle(
                                    color: Colors.black,
                                    fontSize: 20,
                                  )),
                            ),
                            Container(
                                width: double.infinity,
                                padding: const EdgeInsets.fromLTRB(0, 0, 0, 4),
                                child: Text(todo["name"].toString(),
                                    textAlign: TextAlign.left,
                                    style: TextStyle(
                                        color: Colors.black,
                                        fontSize: 30,
                                        fontWeight: FontWeight.bold))),
                            Container(
                              width: double.infinity,
                              padding: const EdgeInsets.fromLTRB(0, 10, 0, 4),
                              child: Text("Done:",
                                  textAlign: TextAlign.left,
                                  style: TextStyle(
                                    color: Colors.black,
                                    fontSize: 20,
                                  )),
                            ),
                            Container(
                              width: double.infinity,
                              padding: const EdgeInsets.fromLTRB(0, 0, 0, 4),
                              child: Text(todo["done"].toString(),
                                  textAlign: TextAlign.left,
                                  style: TextStyle(
                                      color: Colors.black,
                                      fontSize: 30,
                                      fontWeight: FontWeight.bold)),
                            )
                          ],
                        ),
                ),
                floatingActionButton: !editMode
                    ? Mutation(
                        options: MutationOptions(
                          document: gql(deleteTodo),
                          update: (GraphQLDataProxy cache, QueryResult result) {
                            return cache;
                          },
                          onCompleted: (dynamic resultData) {
                            print(resultData);
                            refresh();
                            ScaffoldMessenger.of(context)
                                .showSnackBar(SnackBar(content: Text('Done.')));
                            Navigator.pop(context);
                          },
                        ),
                        builder: (
                          RunMutation runMutation,
                          QueryResult result,
                        ) {
                          return Container(
                              child: Column(
                                  crossAxisAlignment: CrossAxisAlignment.end,
                                  mainAxisAlignment: MainAxisAlignment.end,
                                  children: [
                                Padding(
                                    padding: EdgeInsets.fromLTRB(0, 0, 0, 5),
                                    child: FloatingActionButton(
                                      mini: true,
                                      heroTag: null,
                                      child: Icon(Icons.delete),
                                      onPressed: () {
                                        runMutation({'id': id});
                                        ScaffoldMessenger.of(context)
                                            .showSnackBar(SnackBar(
                                                content:
                                                    Text('Deleting todo...')));
                                      },
                                    )),
                                FloatingActionButton(
                                  onPressed: () {
                                    setState(() {
                                      editMode = true;
                                    });
                                  },
                                  tooltip: 'Edit todo',
                                  child: Icon(Icons.edit),
                                )
                              ]));
                        })
                    : Mutation(
                        options: MutationOptions(
                          document: gql(updateTodo),
                          update: (GraphQLDataProxy cache, QueryResult result) {
                            return cache;
                          },
                          onCompleted: (dynamic resultData) {
                            print(resultData);
                            refresh();
                            refetch();
                            ScaffoldMessenger.of(context)
                                .showSnackBar(SnackBar(content: Text('Done.')));
                          },
                        ),
                        builder: (
                          RunMutation runMutation,
                          QueryResult result,
                        ) {
                          return Container(
                              child: Column(
                                  crossAxisAlignment: CrossAxisAlignment.end,
                                  mainAxisAlignment: MainAxisAlignment.end,
                                  children: [
                                Padding(
                                    padding: EdgeInsets.fromLTRB(0, 0, 0, 5),
                                    child: FloatingActionButton(
                                      mini: true,
                                      heroTag: null,
                                      child: Icon(Icons.cancel),
                                      onPressed: () {
                                        setState(() {
                                          editMode = false;
                                        });
                                      },
                                    )),
                                FloatingActionButton(
                                  heroTag: null,
                                  child: Icon(Icons.save),
                                  onPressed: () {
                                    ScaffoldMessenger.of(context).showSnackBar(
                                        SnackBar(
                                            content: Text('Updating todo...')));
                                    runMutation({
                                      'id': id,
                                      'name': myController.text,
                                      'done': done
                                    });
                                    setState(() {
                                      editMode = false;
                                    });
                                  },
                                )
                              ]));
                        }),
              );
            }));
  }
}

We hebben drie stringvariabelen ingesteld readTodo , updateTodo , en deleteTodo . De readTodo is een querytekenreeks om een ​​todo op basis van zijn id te retourneren. De updateTodo is een mutatie om een ​​taak bij te werken met behulp van zijn id met nieuwe done en name waarden. De deleteTodo is ook een mutatie die een taak verwijdert.

Zorg ervoor dat de ViewTodo is een stateful widget en beheert de staat in de ViewTodoState widget. Elke variabele binnen de ViewTodoState widget is een toestandsvariabele die kan worden bijgewerkt tijdens de levensduur van de widget.

De constructor is ingesteld om de Todo's is en een refresh-functie te accepteren. In de ViewTodoState widget, zie dat we een editMode . hebben booleaanse variabele. Deze variabele stelt de bewerkingsmodus van de widget in.
We hebben dit gedaan om te schakelen tussen tekstvelden die we kunnen gebruiken om deze widget te bewerken zonder dat er een ander widgetscherm nodig is.

De myController is een tekstcontroller voor een tekstveld bij het bewerken van de taak in een bewerkingsmodus. We gebruiken het om de waarde in een TextField te typen.

De bool done; wordt gebruikt om de done . vast te houden veld van de taak.

In de build methode, hebben we de hele widget in de boomstructuur ingesloten met de Query widget. Het roept de readTodo bij het opstarten en geeft de naam en voltooide velden van de taak weer in de gebruikersinterface.

We gebruikten een ternaire operator om te controleren wanneer de editMode is actief en geeft het tekstveld weer en het selectievakje om de taak te bewerken. Als er geen bewerkingsmodus is, worden de taakdetails weergegeven in tekstwidgets.
We gebruiken ook de editMode om FloatingActionButtons weer te geven gebaseerd op het huidige model.

Als er een bewerkingsmodus is, is de save en cancel FloatingActionButtons zal tonen. De save FloatingActionButton slaat de bewerkte taak op. Het verzamelt de name waarde uit TextField en verzamel de done waarde uit de staat van de CheckBox. Dan zal het de runMutation . aanroepen met de waarden.

Zie dat in de onCompleted functie van de Mutation object dat de bewerkingssectie van de save . omsluit en cancel FloatingActionButton .

We noemen de refresh methode om de lijst met taken in de TodoList . te vernieuwen en de refetch methode uit de Query widget om deze ViewTodo te vernieuwen widget omdat de huidige taak is gewijzigd.

...
onCompleted: (dynamic resultData) {
  print(resultData);
  refresh();
  refetch();
  ScaffoldMessenger.of(context)
      .showSnackBar(SnackBar(content: Text('Done.')));
},
...

Als er geen bewerkingsmodus is, wordt de edit en delete FB's worden getoond. De edit FB, wanneer erop wordt geklikt, stelt de editMode . in Staat tot true . De delete FB, wanneer erop wordt geklikt, verzendt de deleteTodo om de huidige taak te verwijderen.

Zie dat in de onCompleted functie van de Mutation widget die het bijvoegde, noemden we de refetch methode en popte de ViewTodo widget van het scherm omdat deze is verwijderd en niet langer beschikbaar is.

...
onCompleted: (dynamic resultData) {
  print(resultData);
  refresh();
  ScaffoldMessenger.of(context)
      .showSnackBar(SnackBar(content: Text('Done.')));
  Navigator.pop(context);
},
...

Laten we de CreateTodo . coderen scherm.

CreateTodo
Deze methode is waar we nieuwe taken maken. Dit scherm heeft een TextField waar we de naam kunnen typen van de taak die moet worden gemaakt. Het zal een MaterialButton . hebben die een mutatie zal uitvoeren wanneer erop wordt geklikt.

Plak de onderstaande code in lib/screens/CreateTodo.dart :

import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import './../GraphQLConfig.dart';

String addTodo = """
  mutation(\$name: String, \$done: Boolean) {
    createTodo(input: { data: { name: \$name, done: \$done } }) {
      todo {
        name
        done
      }
    }
  }
""";

class CreateTodo extends StatelessWidget {
  final myController = TextEditingController();
  final refresh;
  CreateTodo({Key key, this.refresh}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GraphQLProvider(
        client: GraphQLConfiguration.clientToQuery(),
        child: Mutation(
            options: MutationOptions(
              document:
                  gql(addTodo),
              update: (GraphQLDataProxy cache, QueryResult result) {
                return cache;
              },
              onCompleted: (dynamic resultData) {
                refresh();
                ScaffoldMessenger.of(context)
                    .showSnackBar(SnackBar(content: Text('New todo added.')));
                Navigator.pop(context);
              },
            ),
            builder: (
              RunMutation runMutation,
              QueryResult result,
            ) {
              return Scaffold(
                  appBar: AppBar(
                    title: Text("Create Todo"),
                  ),
                  body: Column(children: [
                    Container(
                        alignment: Alignment.centerLeft,
                        padding: const EdgeInsets.fromLTRB(10, 50, 10, 9),
                        child: TextField(
                          controller: myController,
                          decoration: InputDecoration(
                              border: OutlineInputBorder(),
                              hintText: 'Add todo'),
                        )),
                    Row(children: [
                      Expanded(
                          child: Padding(
                              padding: const EdgeInsets.all(10),
                              child: MaterialButton(
                                onPressed: () {
                                  runMutation({
                                    'name': myController.text,
                                    'done': false
                                  });
                                  ScaffoldMessenger.of(context).showSnackBar(
                                      SnackBar(
                                          content: Text('Adding new todo...')));
                                },
                                color: Colors.blue,
                                padding: const EdgeInsets.all(17),
                                child: Text(
                                  "Add",
                                  style: TextStyle(
                                      fontWeight: FontWeight.bold,
                                      color: Colors.white,
                                      fontSize: 20),
                                ),
                              )))
                    ])
                  ]));
            }));
  }
}

Zie dat we een createTodo . hebben mutatie ingesteld. Deze mutatiereeks zal een nieuwe taak maken in onze Strapi.

De CreateTodo is een staatloze widget en beheert geen staat. De constructor accepteert de verversingsfunctie die eraan is doorgegeven en slaat daarin de refresh . op variabel.

De myController is een TextEditingController gebruikt om TextFields te manipuleren.
We wikkelen de widgetboom in GraphQLProvider en Mutation widgets. De document functie zal de gql(createTodo) . uitvoeren functieaanroep wanneer de runMutation argument in zijn builder functie wordt aangeroepen.

In de gebruikersinterface wordt een TextField weergegeven. Hier wordt de nieuwe taaknaam getypt. De myController is ingesteld op het tekstveld. Hierdoor kunnen we de myController . gebruiken om de waarde van het TextField te krijgen.

De MaterialButton heeft een onPressed evenement dat erop is geregistreerd. De handler wordt aangeroepen wanneer de knop wordt ingedrukt. Dit zal de waarde in het TextField ophalen met behulp van de myController . Het zal de runMutation . aanroepen functie die de waarde in het TextField doorgeeft. Hiermee wordt de createTodo . uitgevoerd mutatie waardoor een nieuwe taak in onze Strapi-backend wordt gecreëerd.
De onCompleted functie wordt aangeroepen wanneer de mutatie is voltooid:

De MaterialButton heeft een onPressed evenement dat erop is geregistreerd. De handler wordt aangeroepen wanneer de knop wordt ingedrukt. Dit zal de waarde in het TextField ophalen met behulp van de myController .

Het zal de runMutation-functie aanroepen en de waarde in het TextField doorgeven. Hiermee wordt de createTodo . uitgevoerd mutatie, waardoor een nieuwe taak wordt gecreëerd in onze Strapi-backend.

De onCompleted functie wordt aangeroepen wanneer de mutatie is voltooid:

...
onCompleted: (dynamic resultData) {
  refresh();
  ScaffoldMessenger.of(context)
      .showSnackBar(SnackBar(content: Text('New todo added.')));
  Navigator.pop(context);
},
...

De refresh functie doorgegeven aan de CreateTodo widget van de TodoList widget wordt aangeroepen, dus de takenlijst in de TodoList widget is bijgewerkt om ons nieuw toegevoegde taakitem weer te geven.

Test de app

Todo toevoegen


Todo bekijken

Todo bewerken




Verwijder taak

Referenties

  • graphql_flutter:^4.0.1
  • GraphQL gebruiken in Flutter
  • Hoe u proxy-instellingen voor Android-emulator instelt
  • Android Emulator-netwerken instellen
  • Een eenvoudige applicatie bouwen met Flutter en GraphQL
  • Hoe te verwijzen naar localhost:8000 met het Dart http-pakket in Flutter?
  • De Flutter-app verbinden met Localhost# Conclusie

We hebben veel geleerd in deze tutorial. Ten eerste zijn we begonnen met het leren over Strapi en de voordelen die het met zich meebrengt voor moderne softwareontwikkeling.

Vervolgens leerden we hoe we een Strapi-project kunnen ondersteunen, hoe we er collecties in kunnen bouwen met behulp van het beheerderspaneel en hoe u GraphQL-eindpunten instelt.

We hebben een eenvoudige Todo-app in Flutter gemaakt om te laten zien hoe we Strapi GraphQL-eindpunten kunnen gebruiken vanuit een mobiele app.

Strapi is geweldig. Het is eenvoudig om mee te beginnen en het heeft gemakkelijk te begrijpen documenten. Het kan verbinding maken met elke client, mobiel, internet of desktop.

Broncode

  • Back-end
  • Frontend - Flutter