Build a Personal Target Tracking Application with Flutter Riverpod and Strapi GraphQL
Tags: strapi, flutter, mobile, graphql
Published: 2024-05-29 07:26:03

## Introduction

With the evolution of technology, mobile application development processes are also evolving. In this article, we will explore how using Flutter and Riverpod for Strapi API and GraphQL integration can simplify and even transform application development processes.
The modern application development process with Flutter, Riverpod, and Strapi offers developers flexibility and scalability. With these technologies, you can create user-friendly and high-performance mobile applications. These aspects of application development play a critical role in the success of your project.

## Prerequisites

Before starting the tutorial on developing a personal target tracking application with [Flutter](https://flutter.dev/), [Riverpod](https://riverpod.dev/), [Strapi](https://strapi.io/), and [GraphQL](https://graphql.org/), ensure you meet the following requirements:

- **Development Environment**: Install Flutter and the [Dart SDK](https://dart.dev/get-dart) from flutter.dev. Ensure you have Node.js installed for running Strapi.
- **Knowledge Base**: Basic familiarity with Flutter and Dart for mobile development, understanding of GraphQL for data handling, and experience with Node.js-based CMS like Strapi.
- **Tools**: A code editor like Visual Studio Code or Android Studio, equipped with support for Flutter and JavaScript.

## Why Choose Riverpod for State Management?

In the realm of Flutter development, managing the state of an application can often become complex as the app grows. This is where Flutter Riverpod comes into play, offering a refined and advanced solution for state management that addresses the limitations of its predecessor, [Provider](https://pub.dev/packages/provider). Here’s why Riverpod stands out:

- **Scoped Access to State**: Riverpod allows for the scoped access of state, ensuring widgets have access to the state they need and nothing more. This encapsulation enhances performance and reduces the likelihood of unintended side effects.
- **Flexible State Modification**: Riverpod simplifies state modification with its support for immutable state objects. By embracing the principles of immutability, it ensures that state changes are predictable and manageable.
- **Incorporating Riverpod with** [**Freezed**](https://pub.dev/packages/freezed) **for Immutable State Management**: To leverage Riverpod alongside Freezed in your Flutter project for even more robust state management, you'll be enhancing components like `GoalNotifier` to efficiently handle immutable state. Freezed complements Riverpod by enabling the use of immutable objects in [Dart](https://dart.dev/), which aids in making your state management more predictable and safer.
- **Combining Strapi with GraphQL for your Flutter application**: When managed with Riverpod, creates a robust, efficient, and flexible development ecosystem.
- **Flexibility and Customizability**: Strapi is a headless CMS that provides the flexibility to define your data structures (models), content types, and relations tailored to your application needs. This adaptability is crucial for Flutter apps requiring a custom data set.

Strapi, GraphQL, Flutter, and Riverpod create a cohesive development ecosystem that balances backend flexibility, efficient data management, cross-platform UI development, and robust state management. This combination is particularly potent for building modern, scalable, high-performance mobile applications requiring real-time data updates, custom content structures, and a smooth user experience across multiple platforms.

## Content Flow

### Requirements

- **UI Development**: Flutter widgets can easily create your application's user interface. This shapes the look and feel of your application.
- **State Management**: Developing the core part of your application with a state management system enhances the scalability and maintainability of your app. Riverpod offers a strong and flexible solution in this regard.
- **Backend Integration**: The ease provided by the Strapi API makes backend integration less cumbersome at this stage of the project. Working with Strapi offers a significant advantage in terms of data management and API calls.

### Integration Steps:

- **Setting Up Components**: Create your application's user interface (UI) components.
- **Creating Providers**: Use Riverpod to create providers for state management. These providers manage the data flow between different parts of your application.
- **Adding Routers**: Set up routers to manage transitions between pages. This ensures a smooth navigation experience within the app.
- **Testing with Mock Data**: In the initial stage, test your providers and UI components with mock data. This provides quick feedback on your application's data structures and workflows.

### Transition from Mock Data to Strapi and GraphQL:

- **Strapi Setup and Configuration**: Set up your Strapi project and create the necessary API endpoints. These endpoints define the data types and structures your application needs.
- **Defining GraphQL Schemas**: From the Strapi administration panel, define your GraphQL schemas. These schemas determine the structure of the data your application will query from Strapi.
- **Updating Your Providers**: Update the providers in your application to make queries to real Strapi endpoints instead of mock data. Configure your Riverpod providers to execute your GraphQL queries and connect the data to your application.
- **Queries and Mutations**: Write your GraphQL queries and mutations. These queries enable your application to fetch or send data to Strapi.

Before starting your project, it's essential to have your Flutter development environment properly set up. This requires having the Dart SDK and downloading Flutter directly from its official website (flutter.dev). To verify you are using the latest version of Flutter, run the flutter doctor command in your terminal.


```bash
flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.19.6, on macOS 14.4.1 23E224 darwin-arm64, locale en-DE)
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 15.3)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2023.1)
[✓] VS Code (version 1.88.1)
[✓] Connected device (3 available)
[✓] Network resources
```
Additionally, if you're using an advanced Integrated Development Environment (IDE) like [Visual Studio Code (VSCode)](https://code.visualstudio.com/), you can directly use iOS or Android emulators through the IDE.

We named our project `personal_goals_app`. This name reflects our aim to create an application where users can set personal goals. A clean Flutter setup and the establishment of a state management system with Riverpod greatly facilitate Strapi API integration.

## Set Up Flutter Project

Through the terminal or command prompt, run the command below to create a new Flutter project named `personal_goals_app`.

```bash
flutter create personal_goals_app
```

![001-create-project.png](https://api-prod.strapi.io/uploads/001_create_project_331dc7d09e.png)

Navigate to the created project directory by running the command below:

```bash
cd personal_goals_app
```

Start your application with the command below:

```bash
flutter run
```

This confirms that your first Flutter application is running successfully.

VSCode Command Palette:

![002-vscode-command-pallete.png](https://api-prod.strapi.io/uploads/002_vscode_command_pallete_24de122f7b.png)

VSCode Terminal:

![003-vscode-terminal.png](https://api-prod.strapi.io/uploads/003_vscode_terminal_7f0d9718f0.png)

The `src/goals/components` and `src/goals/provider` directories hold your UI components and state management logic, respectively. This separation makes your code more readable and manageable.

![004-folder-structure.png](https://api-prod.strapi.io/uploads/004_folder_structure_91579b231d.png)

The `src/goals` directory contains your Goal model and general files. The `main.dart` file includes your application's navigation and basic settings.

## Set Up Providers with Riverpod

State management is one of the cornerstones of modern application development. Riverpod stands out for its flexibility and ease of use in this area.
Navigate to your `pubspec.yaml` file and add the following line under dependencies to include Riverpod in your project:

```yml
dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  flutter_riverpod: ^2.5.1
  intl: ^0.18.0
```

### Define `Goal` Model
In the `goal_model.dart` file, define `Goal` model. Use the `Goal` class and `GoalStatus` enum.

```dart
enum GoalStatus { active, completed, pending }

enum GoalCategory { vacation, money, exercise, smoke, language }

class Goal {
  final String id;
  final String name;
  final String description;
  final DateTime startDate;
  final DateTime?
      endDate; // End date is optional because some goals might not have a specific end date
  final GoalCategory category;
  GoalStatus status;
  double?
      targetValue; // Numeric value representing the goal target (e.g., amount to save)
  double?
      currentValue; // Current progress towards the goal (e.g., current savings)

  Goal({
    required this.id,
    required this.name,
    required this.description,
    required this.startDate,
    this.endDate,
    required this.category,
    this.status = GoalStatus.pending,
    this.targetValue,
    this.currentValue,
  });

  // Calculate the status of the goal based on dates
  static GoalStatus calculateStatus(DateTime startDate, DateTime endDate) {
    final currentDate = DateTime.now();
    if (currentDate.isAfter(endDate)) {
      return GoalStatus.completed;
    } else if (currentDate.isAfter(startDate)) {
      return GoalStatus.active;
    } else {
      return GoalStatus.pending;
    }
  }
}

```

### Define Your State with Freezed
Create a new file for your state, e.g., `goal_state.dart`. Use Freezed to define an immutable state class. In this example, the state will directly hold a list of goals, but you could expand it to include other state properties as needed.

```dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:personal_goals_app/src/goals/models/goal_model.dart';
part 'goal_state.freezed.dart';

@freezed
class GoalState with _$GoalState {
  const factory GoalState({
    @Default([]) List<Goal> goals,
  }) = _GoalState;
}

```

### Create a State Notifier:
In the `lib/src/providers` directory, create a file named `goal_provider.dart`. In this file, set up a structure using `StateNotifier` that allows you to add, update, and delete goals.

```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:personal_goals_app/src/goals/models/goal_model.dart';
import 'package:personal_goals_app/src/provider/goal_state.dart';

class GoalNotifier extends StateNotifier<GoalState> {
  GoalNotifier()
      : super(GoalState(goals: [
          Goal(
            id: '1',
            name: 'Vacation in Milan',
            description: 'Enjoy the beauty of Milan',
            startDate: DateTime(2024, 04, 29),
            endDate: DateTime(2024, 11, 1),
            category: GoalCategory.vacation,
            status: GoalStatus.active,
          ),
          Goal(
            id: '2',
            name: 'Quit Smoking',
            description:
                'Reduce cigarette intake gradually and increase smoke-free days',
            startDate: DateTime.now(),
            endDate: DateTime.now().add(const Duration(days: 90)),
            category: GoalCategory.smoke,
          ),
        ]));

  // Add a new goal
  void addGoal(Goal goal) {
    state = state.copyWith(goals: [...state.goals, goal]);
  }

  // Update an existing goal
  void updateGoal(String id, Goal updatedGoal) {
    state = state.copyWith(
      goals: state.goals
          .map((goal) => goal.id == id ? updatedGoal : goal)
          .toList(),
    );
  }

  // Delete a goal
  void deleteGoal(String id) {
    state = state.copyWith(
      goals: state.goals.where((goal) => goal.id != id).toList(),
    );
  }
}

final goalProvider = StateNotifierProvider<GoalNotifier, GoalState>((ref) {
  return GoalNotifier();
});

```

### Wrap Your Application with `ProviderScope`:
In the `main.dart` file, which is the main entry point of your application, wrap your `MaterialApp` widget with `ProviderScope` to make Flutter Riverpod's state management system available throughout your application.

```dart
void main() {
  runApp(const ProviderScope(child: MyApp()));
}

```

## Setting Up Your Router and Components

Flutter operates through the `main.dart` file. In this file, you use the `MaterialApp` widget to bring your application to life and start it with `runApp`. Here, you can set up routing, define themes, and launch your homepage.
In the `main.dart` file, set up the navigation logic that will manage your application's transitions between pages.

```dart
void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Strapi Api Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const HomePage(),
      initialRoute: '/',
      routes: {
        '/start': (context) => StartGoal(),
        '/add': (context) => const GoalFormPage(),
      },
      onGenerateRoute: (settings) {
        if (settings.name == '/edit') {
          final goal = settings.arguments as Goal;
          return MaterialPageRoute(
            builder: (context) {
              return GoalEditPage(goal: goal);
            },
          );
        }
        return null;
      },
    );
  }
}

```

### Create UI Components:
Now we need to expand the component structure in the `src` directory. So `home.dart` will list our goals. Creating detailed components like `goal_add.dart`, `goal_edit.dart`, `goal_start.dart`, `goal_card.dart` will make our work and state management easier as the project progresses.

![005-create-ui-components.png](https://api-prod.strapi.io/uploads/005_create_ui_components_7eca225b64.png)


### Implement Home Page:
Implement the Home page in `home.dart`.

- Display a list of goal cards.
- Use a `FloatingActionButton` to navigate to the “**Add Goal”** page.
- Call the provider here to read data.

```dart
class HomePage extends ConsumerWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final goals = ref.watch(goalProvider).goals;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Targets'),
      ),
      body: ListView.builder(
        itemCount: goals.length,
        itemBuilder: (context, index) {
          final goal = goals[index];
          return GoalCard(goal: goal);
        },
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () {
          Navigator.pushNamed(context, '/start');
        },
        label: const Text('Add New Target'),
        icon: const Icon(Icons.add),
      ),
    );
  }
}

```

### Create Goal Cards:
Create Goal cards in `goal_card.dart`.

- Design a card widget that displays goal information.
- Add buttons or gestures to each card to trigger edit or delete actions.

```dart
class GoalCard extends StatelessWidget {
  final Goal goal;

  const GoalCard({Key? key, required this.goal}) : super(key: key);

  String formatDate(DateTime date) {
    return '${date.month}/${date.year}';
  }

  Color getStatusColor(GoalStatus status) {
    switch (status) {
      case GoalStatus.active:
        return Colors.deepPurple;
      case GoalStatus.pending:
        return Colors.blue;
      case GoalStatus.completed:
        return Colors.green;
      default:
        return Colors.grey;
    }
  }

  @override
  Widget build(BuildContext context) {
    goal.status = Goal.calculateStatus(goal.startDate, goal.endDate!);

    return Card(
      margin: const EdgeInsets.all(24),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Container(
            width: 120,
            color: getStatusColor(goal.status),
            padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
            alignment: Alignment.center,
            child: Text(
              goal.status.toString().split('.').last.toUpperCase(),
              style: const TextStyle(color: Colors.white),
            ),
          ),
          ListTile(
            leading: const Icon(Icons.track_changes),
            title: Text(goal.name),
            subtitle: Text(
              'Target duration: ${goal.endDate?.difference(goal.startDate).inDays ?? 'N/A'} days',
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Expanded(
                  child: Text(
                    "End Date: ${goal.endDate != null ? formatDate(goal.endDate!) : 'N/A'}",
                    textAlign: TextAlign.left,
                  ),
                ),
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Expanded(
                  child: Text(
                    "Description: ${goal.description}",
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
              ],
            ),
          ),
          ButtonBar(
            children: [
              TextButton(
                child: const Text('Go Details'),
                onPressed: () {
                  Navigator.pushNamed(context, '/edit', arguments: goal);
                },
              ),
            ],
          ),
          const SizedBox(height: 40)
        ],
      ),
    );
  }
}

```

![006-create-goal-card.png](https://api-prod.strapi.io/uploads/006_create_goal_card_b97777fcbc.png)

### Build the Start Goal Page in `goal_start.dart`:
Inside the `goal_start.dart` file, build the **"Start Goal”** page.

```dart
class StartGoal extends StatelessWidget {
  StartGoal({super.key});

  final List<GoalList> targetList = [
    GoalList(
      title: 'Plan your vacation',
      icon: Icons.flight_takeoff,
      subtitle: 'Plan your next getaway',
    ),
    GoalList(
      title: 'Save Money',
      icon: Icons.attach_money,
      subtitle: 'Start saving money',
    ),
    GoalList(
      title: 'Quit Smoking',
      icon: Icons.smoke_free,
      subtitle: 'Track smoke-free days',
    ),
    GoalList(
      title: 'Exercise',
      icon: Icons.directions_run,
      subtitle: 'Keep up with your workouts',
    ),
    GoalList(
      title: 'Learn a new language',
      icon: Icons.book,
      subtitle: 'Stay on top of your studies',
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Add a new target')),
      body: ListView.builder(
        itemCount: targetList.length,
        itemBuilder: (BuildContext context, int index) {
          return Card(
            child: ListTile(
              leading: Icon(
                targetList[index].icon,
                size: 36,
                color: Colors.deepPurple,
              ),
              title: Text(targetList[index].title),
              subtitle: Text(targetList[index].subtitle),
              trailing: const Icon(
                Icons.arrow_forward_ios,
                color: Colors.deepPurple,
              ),
              onTap: () {
                Navigator.pushNamed(context, '/add');
              },
            ),
          );
        },
      ),
    );
  }
}

```

![007-start-goal-page.png](https://api-prod.strapi.io/uploads/007_start_goal_page_44db93d33d.png)

### Build the “Add Goal” Page:
Inside the `goal_add.dart`, build the **“Add Goal”** page.

- Create a form for adding a new goal.
- Consider using a `PageView` if you want a step-by-step guide to input the information, but a single form would be simpler and is usually sufficient.

```dart
class GoalFormPage extends ConsumerStatefulWidget {
  const GoalFormPage({Key? key}) : super(key: key);

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

class GoalFormPageState extends ConsumerState<GoalFormPage> {
  final _formKey = GlobalKey<FormState>();
  final TextEditingController _nameController = TextEditingController();
  final TextEditingController _descriptionController = TextEditingController();

  DateTime? _startDate;
  DateTime? _endDate;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text('Add Target')),
        body: Form(
            key: _formKey,
            child: SingleChildScrollView(
                padding: const EdgeInsets.all(16.0),
                child: Column(children: [
                  TextFormField(
                    controller: _nameController,
                    decoration: const InputDecoration(labelText: 'Goal Name'),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please enter a goal name';
                      }
                      return null;
                    },
                  ),
                  TextFormField(
                    controller: _descriptionController,
                    decoration: const InputDecoration(labelText: 'Description'),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please enter a description';
                      }
                      return null;
                    },
                  ),
                  ListTile(
                    title: Text(
                        'Start Date: ${_startDate != null ? DateFormat('yyyy-MM-dd').format(_startDate!) : 'Select'}'),
                    trailing: const Icon(Icons.calendar_today),
                    onTap: () async {
                      final picked = await showDatePicker(
                        context: context,
                        initialDate: DateTime.now(),
                        firstDate: DateTime.now(),
                        lastDate: DateTime(2050),
                      );
                      if (picked != null && picked != _startDate) {
                        setState(() {
                          _startDate = picked;
                        });
                      }
                    },
                  ),
                  ListTile(
                    title: Text(
                        'End Date: ${_endDate != null ? DateFormat('yyyy-MM-dd').format(_endDate!) : 'Select'}'),
                    trailing: const Icon(Icons.calendar_today),
                    onTap: () async {
                      final picked = await showDatePicker(
                        context: context,
                        initialDate: DateTime.now(),
                        firstDate: DateTime(2000),
                        lastDate: DateTime(2100),
                      );
                      if (picked != null && picked != _endDate) {
                        setState(() {
                          _endDate = picked;
                        });
                      }
                    },
                  ),
                  const SizedBox(
                    height: 10,
                  ),
                  ElevatedButton(
                    onPressed: _saveGoal,
                    child: const Text("Save your goal"),
                  )
                ]))));
  }

  void _saveGoal() {
    if (_formKey.currentState!.validate()) {
      final newGoal = Goal(
        id: DateTime.now().millisecondsSinceEpoch.toString(),
        name: _nameController.text,
        description: _descriptionController.text,
        startDate: _startDate ?? DateTime.now(),
        endDate: _endDate,
        category: GoalCategory.vacation,
        status: GoalStatus.active,
      );

      ref.read(goalProvider.notifier).addGoal(newGoal);

      Navigator.pop(context);
    }
  }
}

```

![008-add-goal-page.png](https://api-prod.strapi.io/uploads/008_add_goal_page_3d327468c7.png)

### Craft the Edit Goal Page in goal_edit.dart:

- This will be similar to `goal_add.dart` but for editing existing goals.
- Ensure you pass the `goal` object to be edited to this page.

```dart
class GoalEditPage extends ConsumerStatefulWidget {
  final Goal goal;

  const GoalEditPage({Key? key, required this.goal}) : super(key: key);

  @override
  ConsumerState<GoalEditPage> createState() => _GoalEditFormPageState();
}

class _GoalEditFormPageState extends ConsumerState<GoalEditPage> {
  final _formKey = GlobalKey<FormState>();
  final TextEditingController _nameController = TextEditingController();
  final TextEditingController _descriptionController = TextEditingController();

  DateTime? _startDate;
  DateTime? _endDate;
  @override
  void initState() {
    super.initState();
    _nameController.text = widget.goal.name;
    _descriptionController.text = widget.goal.description;
    _startDate = widget.goal.startDate;
    _endDate = widget.goal.endDate;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text('Edit Target')),
        body: Form(
            key: _formKey,
            child: SingleChildScrollView(
                padding: const EdgeInsets.all(16.0),
                child: Column(children: [
                  TextFormField(
                    controller: _nameController,
                    decoration: const InputDecoration(labelText: 'Goal Name'),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please enter a goal name';
                      }
                      return null;
                    },
                  ),
                  TextFormField(
                    controller: _descriptionController,
                    decoration: const InputDecoration(labelText: 'Description'),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please enter a description';
                      }
                      return null;
                    },
                  ),
                  ListTile(
                    title: Text(
                        'Start Date: ${_startDate != null ? DateFormat('yyyy-MM-dd').format(_startDate!) : 'Select'}'),
                    trailing: const Icon(Icons.calendar_today),
                    onTap: () async {
                      final picked = await showDatePicker(
                        context: context,
                        initialDate: DateTime.now(),
                        firstDate: DateTime(2000),
                        lastDate: DateTime(2100),
                      );
                      if (picked != null && picked != _startDate) {
                        setState(() {
                          _startDate = picked;
                        });
                      }
                    },
                  ),
                  ListTile(
                    title: Text(
                        'End Date: ${_endDate != null ? DateFormat('yyyy-MM-dd').format(_endDate!) : 'Select'}'),
                    trailing: const Icon(Icons.calendar_today),
                    onTap: () async {
                      final picked = await showDatePicker(
                        context: context,
                        initialDate: DateTime.now(),
                        firstDate: DateTime(2000),
                        lastDate: DateTime(2100),
                      );
                      if (picked != null && picked != _endDate) {
                        setState(() {
                          _endDate = picked;
                        });
                      }
                    },
                  ),
                  const SizedBox(
                    height: 10,
                  ),
                  ElevatedButton(
                    onPressed: () {
                      if (_formKey.currentState!.validate()) {
                        Goal updatedGoal = Goal(
                          id: widget.goal.id,
                          name: _nameController.text,
                          description: _descriptionController.text,
                          startDate: _startDate!,
                          endDate: _endDate,
                          category: widget.goal.category,
                          status: widget.goal.status,
                        );

                        ref
                            .read(goalProvider.notifier)
                            .updateGoal(widget.goal.id, updatedGoal);

                        Navigator.pop(context);
                      }
                    },
                    child: const Text("Edit your goal"),
                  ),
                  const SizedBox(
                    height: 20,
                  ),
                  IconButton(
                    color: Theme.of(context).hintColor,
                    icon: Icon(
                      Icons.delete,
                      color: Theme.of(context).primaryColor,
                    ),
                    onPressed: () {
                      if (_formKey.currentState!.validate()) {
                        ref
                            .read(goalProvider.notifier)
                            .deleteGoal(widget.goal.id);

                        Navigator.pop(context);
                      }
                    },
                  )
                ]))));
  }
}

```

![009-edit-goal-page.png](https://api-prod.strapi.io/uploads/009_edit_goal_page_a076efd1e2.png)

## Transition from Mock Data to Strapi and GraphQL:

If you haven't already, start by installing Strapi CMS. You can choose to use Strapi in a project-specific manner or globally. For a new project, running the command below will set up a new Strapi project and start it with a [SQLite](https://www.sqlite.org/) database for quick development.

```bash
npx create-strapi-app my-project --quickstart
```

It's generally a good idea to keep your backend and frontend projects in separate directories to maintain a clear separation of concerns. This separation helps manage dependencies, version control, and deployment processes more efficiently for each part of your application.
In this setup, both your Flutter project (`personal_goals_app`) and your Strapi project (`strapi_goals_app`) are located under the same parent directory (`strapi_flutter`), but they are kept in separate folders.

```bash
strapi_flutter/
│
├── personal_goals_app/ # Your Flutter project
│ ├── lib/
│ ├── android/
│ ├── ios/
│ └── ...
│
└── strapi_goals_app/ # Your Strapi project
├── api/
├── config/
├── extensions/
└── ...
```

![010-install-strapi.png](https://api-prod.strapi.io/uploads/010_install_strapi_a860f1323b.png)

### Setting up Strapi Content Types
Before you begin interacting with data in your Flutter app, you must define the appropriate content types in Strapi that mirror the structure of your app's goals.

1. Log in to Strapi to access your Strapi admin panel.
   ![011-log-in-to-strapi.png](https://api-prod.strapi.io/uploads/011_log_in_to_strapi_55ec7c1d22.png)

2. Head to the "Content-Types Builder" section.
3. Create a new content type named `Goal` .

 ![012-create-collection-type.png](https://api-prod.strapi.io/uploads/012_create_collection_type_0e7d6eb4da.png)

4. Add fields corresponding to your Flutter app's goal model, such as:

- Name (Text)
- Description (Text)
- Start Date and End Date (Date)
- Category (Enumeration)
- Status (Enumeration with values like active, completed, pending, drafted).

 ![013-collection-type-fields.png](https://api-prod.strapi.io/uploads/013_collection_type_fields_143c934a60.png)

5. Save Content Type: Save the content type, and Strapi will automatically restart.

### Setting Permissions
In the **_Settings > Users & Permissions plugin > Roles_** section, configure the public role (or your preferred role) to have permissions to create, read, update, and delete entries for the Goal content types. This step is crucial for enabling interaction between your Flutter app and Strapi.

![014-set-permissions.png](https://api-prod.strapi.io/uploads/014_set_permissions_cdfcc0226c.png)

### Defining GraphQL Schemas

- Enable GraphQL: Note that in Strapi v4, the GraphQL plugin is not installed by default. Install it by running the command below in your Strapi project directory.

```bash
npm run strapi install graphql
```

Strapi will auto-generate the GraphQL schema based on your content types, accessible at `/graphql` endpoint on your Strapi server.

See more here: https://pub.dev/packages/graphql_flutter

The final shape of my yaml file:

```yaml
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  flutter_riverpod: ^2.5.1
  build_runner: ^2.4.9
  freezed: ^2.4.7
  freezed_annotation: ^2.4.1
  intl: ^0.18.0
  graphql_flutter: ^5.1.0 —>newly added
```

- Exploring with GraphQL Playground

Utilize the built-in GraphQL Playground at http://localhost:1337/graphql to explore schemas, test queries, and mutations. Define all necessary queries and mutations for your Flutter app, test them, and observe changes in Strapi.

![015-graphql-playground.png](https://api-prod.strapi.io/uploads/015_graphql_playground_6e4d794542.png)

### Update Flutter App for Strapi and GraphQL Integration
Replace mock data in your `GoalNotifier` with real data fetched from Strapi.

- Creating `goal_graphql_provider.dart`: Create a provider to handle GraphQL client calls.

```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:personal_goals_app/graphql_client.dart';

final graphqlClientProvider = Provider<GraphQLClient>((ref) {
  final ValueNotifier<GraphQLClient> client = graphqlClient;
  return client.value;
});

```

- Creating `graphql_client.dart`: Define the GraphQL client with the Strapi GraphQL URL.

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

ValueNotifier<GraphQLClient> initializeClient(String graphqlEndpoint) {
  final HttpLink httpLink = HttpLink(graphqlEndpoint);
  return ValueNotifier(
    GraphQLClient(
      link: httpLink,
      cache: GraphQLCache(store: InMemoryStore()),
    ),
  );
}

const String strapiGraphQLURL = 'http://localhost:1337/graphql';
final graphqlClient = initializeClient(strapiGraphQLURL);

```

### Creating GraphQL Mutations and Queries
To enable communication between your Flutter app and the Strapi backend, you'll need to define Strapi GraphQL mutations and queries that correspond to the actions you want to perform on the Goal content type.

1. Mutations (`mutations.dart`)
   In this file, you'll define Strapi GraphQL mutations for creating, updating, and deleting goals.

- Create Goal Mutation: This mutation creates a new goal in the Strapi database. It includes parameters for the goal's name, description, start date, end date, category, and status.
- Update Goal Mutation: This mutation updates an existing goal in the Strapi database. It includes parameters for the goal's ID and updated fields such as name, description, start date, end date, category, and status.
- Delete Goal Mutation: This mutation deletes a goal from the Strapi database based on its ID.

```dart
// Create a new goal
const String createGoalMutation = """
mutation CreateGoal(\$name: String!, \$description: String!, \$startDate: Date!, \$endDate: Date, \$category: ENUM_GOAL_CATEGORY!, \$status: ENUM_GOAL_STATUS!) {
  createGoal(data: {
    name: \$name,
    description: \$description,
    startDate: \$startDate,
    endDate: \$endDate,
    category: \$category,
    status: \$status
  }) {
    data {
      id
      attributes {
        name
        description
        startDate
        endDate
        category
        status
      }
    }
  }
}
""";

// Update an existing goal
const String updateGoalMutation = """
mutation UpdateGoal(\$id: ID!, \$name: String, \$description: String, \$startDate: Date, \$endDate: Date, \$category: ENUM_GOAL_CATEGORY, \$status: ENUM_GOAL_STATUS) {
  updateGoal(id: \$id, data: {
    name: \$name,
    description: \$description,
    startDate: \$startDate,
    endDate: \$endDate,
    category: \$category,
    status: \$status
  }) {
    data {
      id
      attributes {
        name
        description
        startDate
        endDate
        category
        status
      }
    }
  }
}
""";

// Delete a goal
const String deleteGoalMutation = """
mutation DeleteGoal(\$id: ID!) {
  deleteGoal(id: \$id) {
    data {
      id
    }
  }
}
""";
```

2. Queries (`queries.dart`)
   In this file, you'll define a GraphQL query for fetching all goals from the Strapi database.

- Get Goals Query: This query fetches all goals stored in the Strapi database. It retrieves the ID, name, description, start date, end date, category, and status for each goal.

```dart
const String getGoalsQuery = """
query GetGoals {
  goals {
    data {
      id
      attributes {
        name
        description
        startDate
        endDate
        category
        status
      }
    }
  }
}
""";
```

### Updating Riverpod Provider
To integrate these mutations and queries into your Flutter app, you'll need to update the Riverpod provider (`goalProvider`) to use the real queries and mutations defined above. This provider is responsible for managing the state of goals in your app and facilitating communication with the Strapi backend through GraphQL mutations and queries.
In summary, by defining GraphQL mutations and queries and updating your Riverpod provider to use them, you'll enable your Flutter app to interact seamlessly with the Strapi backend, allowing users to perform actions such as creating, updating, and deleting goals.

```dart
 import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:intl/intl.dart';
import 'package:personal_goals_app/src/goals/models/goal_model.dart';
import 'package:personal_goals_app/src/graphql/mutations.dart';
import 'package:personal_goals_app/src/graphql/queries.dart';
import 'package:personal_goals_app/src/provider/goal_graphql_provider.dart';
import 'package:personal_goals_app/src/provider/goal_state.dart';

class GoalNotifier extends StateNotifier<GoalState> {
  final GraphQLClient client;

  GoalNotifier(this.client) : super(const GoalState(goals: []));

  //Get all goals
  Future<void> getGoals() async {
    final QueryOptions options = QueryOptions(
      document: gql(getGoalsQuery),
    );

    final QueryResult result = await client.query(options);

    if (result.hasException) {
      print("Exception fetching goals: ${result.exception.toString()}");
      return;
    }

    final List<dynamic> fetchedGoals = result.data?['goals']['data'] ?? [];
    final List<Goal> goalsList =
        fetchedGoals.map((goalData) => Goal.fromJson(goalData)).toList();

    state = state.copyWith(goals: goalsList);
  }

  // Add a new goal
  Future<void> addGoal(Goal goal) async {
    final MutationOptions options = MutationOptions(
      document: gql(createGoalMutation),
      variables: {
        'name': goal.name,
        'description': goal.description,
        'startDate': DateFormat('yyyy-MM-dd').format(goal.startDate),
        'endDate': goal.endDate != null
            ? DateFormat('yyyy-MM-dd').format(goal.endDate!)
            : null,
        'category': goal.category.toString().split('.').last,
        'status': goal.status.toString().split('.').last,
      },
    );

    final QueryResult result = await client.mutate(options);

    if (result.hasException) {
      print("Exception adding goal: ${result.exception.toString()}");
      return;
    }

    final newGoalData = result.data?['createGoal']['data'];
    if (newGoalData != null) {
      final newGoal = Goal.fromJson(newGoalData);
      state = state.copyWith(goals: [...state.goals, newGoal]);
    }
  }

  // Update an existing goal
  Future<void> updateGoal(String id, Goal updatedGoal) async {
    final MutationOptions options = MutationOptions(
      document: gql(updateGoalMutation),
      variables: {
        'id': id,
        'name': updatedGoal.name,
        'description': updatedGoal.description,
        'startDate': DateFormat('yyyy-MM-dd').format(updatedGoal.startDate),
        'endDate': updatedGoal.endDate != null
            ? DateFormat('yyyy-MM-dd').format(updatedGoal.endDate!)
            : null,
        'category': updatedGoal.category.toString().split('.').last,
        'status': updatedGoal.status.toString().split('.').last,
      },
    );

    final QueryResult result = await client.mutate(options);

    if (result.hasException) {
      print("Exception updating goal: ${result.exception.toString()}");
      return;
    }

    await getGoals();
  }

// Delete a goal
  Future<void> deleteGoal(String id) async {
    final MutationOptions options = MutationOptions(
      document: gql(deleteGoalMutation),
      variables: {'id': id},
    );

    final QueryResult result = await client.mutate(options);

    if (result.hasException) {
      print("Exception deleting goal: ${result.exception.toString()}");
      return;
    }

    state = state.copyWith(
        goals: state.goals.where((goal) => goal.id != id).toList());
  }
}

final goalProvider = StateNotifierProvider<GoalNotifier, GoalState>((ref) {
  final client = ref.read(graphqlClientProvider);
  return GoalNotifier(client);
});

```

### Updating Goal Model
In the process of integrating GraphQL queries and mutations to interact with a Strapi backend, several enhancements have been made to the `Goal` model. These enhancements aim to optimize data handling, ensure compatibility with GraphQL operations, and align with Strapi's data structure. Let's delve into the specific changes made to accommodate these requirements:

- With the introduction of GraphQL queries, data is often retrieved in JSON format. To efficiently parse JSON data into the Goal model, a factory method `fromJson` has been added. This method takes a Map representing JSON data and constructs a `Goal` object from it. This enhancement enables seamless conversion of JSON data retrieved from GraphQL queries into `Goal` objects within the Flutter application.
- The `GoalStatus` and `GoalCategory` enums play a crucial role in representing the status and category of goals. To enhance the model's versatility and compatibility with GraphQL and Strapi, methods `_stringToGoalCategory` and `_stringToGoalStatus` have been introduced. These methods convert string representations of enums retrieved from JSON data into their corresponding enum values. By incorporating these methods into the JSON parsing process, the model ensures consistent handling of enumerated types across different data sources and operations.

```dart
enum GoalStatus { active, completed, pending }

enum GoalCategory { vacation, money, exercise, smoke, language }

class Goal {
  final String id;
  final String name;
  final String description;
  final DateTime startDate;
  final DateTime?
      endDate; // End date is optional because some goals might not have a specific end date
  final GoalCategory category;
  GoalStatus status;
  double?
      targetValue; // Numeric value representing the goal target (e.g., amount to save)
  double?
      currentValue; // Current progress towards the goal (e.g., current savings)

  Goal({
    required this.id,
    required this.name,
    required this.description,
    required this.startDate,
    this.endDate,
    required this.category,
    this.status = GoalStatus.pending,
    this.targetValue,
    this.currentValue,
  });

  factory Goal.fromJson(Map<String, dynamic> json) {
    var attributes = json['attributes'];
    return Goal(
      id: json['id'].toString(), // Ensuring `id` is treated as a String.
      name: attributes['name'] ??
          '', // Providing a default empty string if `name` is null.
      description: attributes['description'] ?? '',
      startDate: DateTime.parse(attributes['startDate']),
      endDate: attributes['endDate'] != null
          ? DateTime.parse(attributes['endDate'])
          : null,
      category: _stringToGoalCategory(attributes['category'] ?? 'vacation'),
      status: _stringToGoalStatus(attributes['status'] ?? 'pending'),
      targetValue: attributes['targetValue'],
      currentValue: attributes['currentValue'],
    );
  }
  // Calculate the status of the goal based on dates
  static GoalStatus calculateStatus(DateTime startDate, DateTime endDate) {
    final currentDate = DateTime.now();
    if (currentDate.isAfter(endDate)) {
      return GoalStatus.completed;
    } else if (currentDate.isAfter(startDate)) {
      return GoalStatus.active;
    } else {
      return GoalStatus.pending;
    }
  }

  static GoalCategory _stringToGoalCategory(String category) {
    return GoalCategory.values.firstWhere(
      (e) => e.toString().split('.').last == category,
      orElse: () => GoalCategory.vacation,
    );
  }

  static GoalStatus _stringToGoalStatus(String status) {
    return GoalStatus.values.firstWhere(
      (e) => e.toString().split('.').last == status,
      orElse: () => GoalStatus.pending,
    );
  }
}

```

### Fetching Data from Strapi in `HomePage`
To display the goals fetched from Strapi in your Flutter app, you'll need to call the published data from Strapi in your home page (`home.dart`).

```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:personal_goals_app/src/goals/components/goal_card.dart';
import 'package:personal_goals_app/src/provider/goal_provider.dart';

class HomePage extends ConsumerStatefulWidget {
  const HomePage({Key? key}) : super(key: key);

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

class HomePageState extends ConsumerState<HomePage> {
  @override
  void initState() {
    super.initState();
    Future.microtask(() => ref.read(goalProvider.notifier).getGoals());
  }

  @override
  Widget build(BuildContext context) {
    final goals = ref.watch(goalProvider).goals;

    return Scaffold(
        appBar: AppBar(title: const Text('Targets')),
        body: ListView.builder(
          itemCount: goals.length,
          itemBuilder: (context, index) {
            final goal = goals[index];
            return GoalCard(goal: goal);
          },
        ),
        floatingActionButton: FloatingActionButton.extended(
          onPressed: () async {
            final refreshNeeded = await Navigator.pushNamed(context, '/start');
            if (refreshNeeded == true) {
              ref.read(goalProvider.notifier).getGoals();
            }
          },
          label: const Text('Add New Target'),
          icon: const Icon(Icons.add),
        ));
  }
}

```

In this file, you'll use the `ConsumerWidget` provided by Riverpod to fetch the goals from Strapi and display them in a list view.
Fetching Goals: Inside the build method, you'll call the `getGoals` method from the `goalProvider` notifier to fetch the goals from Strapi. The `ref.watch(goalProvider)` statement will ensure that the widget rebuilds whenever the state of the `goalProvider` changes. By following this approach, you'll have a clean and efficient way to fetch and display the goals from Strapi in your Flutter app's home page.

> **NOTE**: Lastly, ensure that the draft mode is disabled in Strapi to see the published data in your app.


![016-edit-configuration.png](https://api-prod.strapi.io/uploads/016_edit_configuration_abd357f0b1.png)

This integration enables seamless communication between your Flutter app and Strapi CMS, allowing users to view and interact with the goals stored in the backend.

## Demo Time!
By the end of this tutorial, you should have a working personal tracking application that allows a user add, start and edit a goal or target.

![personal-tracker-gif.gif](https://api-prod.strapi.io/uploads/personal_tracker_gif_d57e162343.gif)

## Conclusion
Strapi API provides a powerful and customizable API for managing content and data. With Strapi, we can define custom content types, set permissions, and expose APIs tailored to our application's needs. Personally, It is very easy to use and quick to learn.
Benefits of Using Riverpod, Flutter, GraphQL, and Strapi Together:

- **Efficiency:** Flutter Riverpod and GraphQL enable efficient state management and data fetching, reducing unnecessary network requests and enhancing app performance.
- **Flexibility:** GraphQL's flexible query language and Strapi's customizable APIs empower developers to tailor data interactions precisely to their application's requirements.
- **Scalability:** With Riverpod, Flutter, GraphQL, and Strapi, applications can easily scale to accommodate growing user bases and evolving feature sets.
- **Productivity:** The combination of these technologies streamlines development workflows, allowing developers to focus on building features rather than managing complex data flows.

## Additional Resources:

- https://riverpod.dev/docs/introduction/getting_started
- https://graphql.org/learn/
- https://strapi.io/blog/how-to-build-a-simple-crud-application-using-flutter-and-strapi