【Flutter】Cognito with Amplify(Gen2)+CRUD(DynamoDB)

flutter

1. 概要

上記では、table_calendarを使い、カレンダーを表示する内容でした。今回は、AWS Amplify(Gen2)を使い、認証(Cognito)を行うのとTodoアプリを実装する内容となります。

対象としては開発を1年程やってて自分で最初から開発してみたい方になります。そのため細かい用語などの説明はしません。

  • AWS Amplify
    • フルスタック TypeScript。AWS のためのフロントエンド DX
  • こちらの内容をそのまま実施してみます。

2. プロジェクトの準備

2-1. プロジェクトを作成

3. 前提条件

3-1. こちらを参考

4. Amplifyプロジェクトを生成

4-1. 上記2で作成したプロジェクトのベースディレクトリにて実行

npm create amplify@latest -y

※ベースディレクトリに「amplify」ディレクトリが作成される

  • amplify
    • auth
      • resource.ts
    • data
      • resource.ts
    • backend.ts
    • package.json

4-2. 下記コマンドを実行

npx ampx sandbox --outputs-format dart --outputs-out-dir lib

※下記ファイルが作成される

  • amplify_outputs.json
  • lib/amplify_outputs.dart

5. Dependenciesを追加

5-1. Dependenciesを追加

※pubspec.yaml

  • dependencies:
    • amplify_flutter: ^2.0.0
    • amplify_auth_cognito: ^2.0.0
    • amplify_authenticator: ^2.0.0
    • amplify_api: ^2.0.0

5-2. Dependenciesをインストール

flutter pub get

※VSCodeの場合は保存で自動実行されるため、不要

6. 認証

※上記4で自動生成された「amplify/auth/resource.ts」が使われる。

7. Dataの追加

7-1. amplify/data/resource.tsを修正

import { type ClientSchema, a, defineData } from '@aws-amplify/backend';

/*== STEP 1 ===============================================================
The section below creates a Todo database table with a "content" field. Try
adding a new "isDone" field as a boolean. The authorization rule below
specifies that any unauthenticated user can "create", "read", "update", 
and "delete" any "Todo" records.
=========================================================================*/
const schema = a.schema({
  Todo: a
    .model({
      content: a.string(),
      isDone: a.boolean(),
    })
    .authorization(allow => [allow.owner()]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: "userPool",
  },
});

/*== STEP 2 ===============================================================
Go to your frontend source code. From your client-side code, generate a
Data client to make CRUDL requests to your table. (THIS SNIPPET WILL ONLY
WORK IN THE FRONTEND CODE FILE.)

Using JavaScript or Next.js React Server Components, Middleware, Server 
Actions or Pages Router? Review how to generate Data clients for those use
cases: https://docs.amplify.aws/gen2/build-a-backend/data/connect-to-API/
=========================================================================*/

/*
"use client"
import { generateClient } from "aws-amplify/data";
import type { Schema } from "@/amplify/data/resource";

const client = generateClient<Schema>() // use this Data client for CRUDL requests
*/

/*== STEP 3 ===============================================================
Fetch records from the database and use them in your frontend component.
(THIS SNIPPET WILL ONLY WORK IN THE FRONTEND CODE FILE.)
=========================================================================*/

/* For example, in a React component, you can use this snippet in your
  function's RETURN statement */
// const { data: todos } = await client.models.Todo.list()

// return <ul>{todos.map(todo => <li key={todo.id}>{todo.content}</li>)}</ul>

7-2. modelクラスの自動生成

※下記コマンドを実行

npx ampx generate graphql-client-code --format modelgen --model-target dart --out lib/models

※下記ファイルが生成される

  • lib/models
    • ModelProvider.dart
    • Todo.dart

8. ソースコード

8-1. lib/todo_screen.dart

import 'package:flutter/material.dart';
import 'package:amplify_api/amplify_api.dart';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:amplify_test/models/Todo.dart';

class TodoScreen extends StatefulWidget {
  const TodoScreen({super.key});

  @override
  State<TodoScreen> createState() => _TodoScreenState();
}

class _TodoScreenState extends State<TodoScreen> {
  List<Todo> _todos = [];

  @override
  void initState() {
    super.initState();
    _refreshTodos();
  }

  Future<void> _refreshTodos() async {
    try {
      final request = ModelQueries.list(Todo.classType);
      final response = await Amplify.API.query(request: request).response;

      final todos = response.data?.items;
      if (response.hasErrors) {
        safePrint('errors: ${response.errors}');
        return;
      }
      setState(() {
        _todos = todos!.whereType<Todo>().toList();
      });
    } on ApiException catch (e) {
      safePrint('Query failed: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton.extended(
        label: const Text('Add Random Todo'),
        onPressed: () async {
          final newTodo = Todo(
            id: uuid(),
            content: "Random Todo ${DateTime.now().toIso8601String()}",
            isDone: false,
          );
          final request = ModelMutations.create(newTodo);
          final response = await Amplify.API.mutate(request: request).response;
          if (response.hasErrors) {
            safePrint('Creating Todo failed.');
          } else {
            safePrint('Creating Todo successful.');
          }
          _refreshTodos();
        },
      ),
      body: _todos.isEmpty == true
          ? const Center(
              child: Text(
                "The list is empty.\nAdd some items by clicking the floating action button.",
                textAlign: TextAlign.center,
              ),
            )
          : ListView.builder(
              itemCount: _todos.length,
              itemBuilder: (context, index) {
                final todo = _todos[index];
                return Dismissible(
                  key: UniqueKey(),
                  confirmDismiss: (direction) async {
                    if (direction == DismissDirection.endToStart) {
                      final request = ModelMutations.delete(todo);
                      final response =
                          await Amplify.API.mutate(request: request).response;
                      if (response.hasErrors) {
                        safePrint('Updating Todo failed. ${response.errors}');
                      } else {
                        safePrint('Updating Todo successful.');
                        await _refreshTodos();
                        return true;
                      }
                    }
                    return false;
                  },
                  child: CheckboxListTile.adaptive(
                    value: todo.isDone,
                    title: Text(todo.content!),
                    onChanged: (isChecked) async {
                      final request = ModelMutations.update(
                        todo.copyWith(isDone: isChecked!),
                      );
                      final response =
                          await Amplify.API.mutate(request: request).response;
                      if (response.hasErrors) {
                        safePrint('Updating Todo failed. ${response.errors}');
                      } else {
                        safePrint('Updating Todo successful.');
                        await _refreshTodos();
                      }
                    },
                  ),
                );
              },
            ),
    );
  }
}

8-2. lib\main.dart

import 'package:amplify_api/amplify_api.dart';
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_authenticator/amplify_authenticator.dart';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:amplify_test/models/ModelProvider.dart';
import 'package:amplify_test/todo_screen.dart';
import 'package:flutter/material.dart';
import 'amplify_outputs.dart';

Future<void> main() async {
  try {
    WidgetsFlutterBinding.ensureInitialized();
    await _configureAmplify();
    runApp(const MyApp());
  } on AmplifyException catch (e) {
    runApp(Text("Error configuring Amplify: ${e.message}"));
  }
}

Future<void> _configureAmplify() async {
  try {
    await Amplify.addPlugins(
      [
        AmplifyAuthCognito(),
        AmplifyAPI(
          options: APIPluginOptions(
            modelProvider: ModelProvider.instance,
          ),
        ),
      ],
    );
    await Amplify.configure(amplifyConfig);
    safePrint('Successfully configured');
  } on Exception catch (e) {
    safePrint('Error configuring Amplify: $e');
  }
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return Authenticator(
      child: MaterialApp(
        builder: Authenticator.builder(),
        home: const SafeArea(
          child: Scaffold(
            body: Column(
              children: [
                SignOutButton(),
                Expanded(child: TodoScreen()),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

機能

  • 認証
    • メールアドレス
    • パスワード
  • データ操作(Todo)
    • Create
    • Read
    • Update
    • Delete

9. ディレクトリ構成

.
├── README.md
├── amplify
│   ├── auth
│   │   └── resource.ts
│   ├── backend.ts
│   ├── data
│   │   └── resource.ts
│   ├── package.json
│   └── tsconfig.json
├── amplify_outputs.json
├── amplify_test.iml
├── analysis_options.yaml
├── devtools_options.yaml
├── lib
│   ├── amplify_outputs.dart
│   ├── main.dart
│   ├── models
│   │   ├── ModelProvider.dart
│   │   └── Todo.dart
│   └── todo_screen.dart
├── package-lock.json
├── package.json
├── pubspec.lock
└── pubspec.yaml

6 directories, 19 files

10. 結果

11. 備考

Quickstartのそのままですが、AWS Amplify(Gen2)を使い、認証(Cognito)を行うのとTodoアプリを実装する内容でした。

12. 参考

投稿者プロフィール

Sondon
開発好きなシステムエンジニアです。
卓球にハマってます。

関連記事

  1. 【Widget of the Week】#5 Opacity

  2. flutter

    【Flutter】Widgetテスト(Flutter,VSCode,T…

  3. flutter

    【Flutter】アプリにAdmobバナー広告を設置

  4. flutter

    【Flutter】コード自動生成とGraphQLClientを使ってG…

  5. flutter

    【Flutter】画面が開く前にローディングを表示

  6. flutter

    【Flutter】StateNotifierProviderで状態管理…

最近の記事

  1. flutter

制作実績一覧

  1. Checkeys