DEV Community

Cover image for Flutter x Amplify AI Kit Recipe App Development

Flutter x Amplify AI Kit Recipe App Development

This article presents another avenue for Flutter developers to engage with Gen-AI. It demonstrates how to utilize Amazon Bedrock from Flutter via the AWS Amplify AI Kit.

While the official AWS Amplify documentation does not address this topic, there is a hidden path to integrating powerful AI capabilities into your Flutter projects using the Amplify AI Kit. This article will guide you through this uncharted territory.

I present a recipe recommendation app for the Flutter version based on the Amplify AI Kit documentation in this piece.

Here's what you can expect to find:

  • Unofficial integration: Learn how to bridge the gap between Flutter and Amplify AI Kit, unlocking a world of AI possibilities for your applications.
  • Practical application: Create a fully functional recipe recommendation app with AI-powered suggestions.
  • Backend connection: Discover how to seamlessly connect your Flutter frontend with Amazon Bedrock through AWS AppSync.

Sample code

flutter_amplify_ai_kit

A new Flutter project.

Getting Started

This project is a starting point for a Flutter application.

A few resources to get you started if this is your first Flutter project:

For help getting started with Flutter development, view the online documentation, which offers tutorials, samples, guidance on mobile development, and a full API reference.




Preparation

Please set up these tools.

$ flutter --version
Flutter 3.27.4 • channel stable • https://github.com/flutter/flutter.git
Framework • revision d8a9f9a52e (9 days ago) • 2025-01-31 16:07:18 -0500
Engine • revision 82bd5b7209
Tools • Dart 3.6.2 • DevTools 2.40.3

$ node --version
v20.18.1

$ npm --version 
10.8.2
Enter fullscreen mode Exit fullscreen mode

Steps

Create a project

$ flutter create flutter_amplify_ai_kit
...

$ cd flutter_amplify_ai_kit

$ flutter pub add amplify_api amplify_flutter amplify_auth_cognito amplify_authenticator

$ npm create amplify@latest
? Where should we create your project? (.) # Enter
Enter fullscreen mode Exit fullscreen mode

Set up your AWS credentials.

Then, run Amplify Sandbox.

$ npx ampx sandbox --outputs-format dart --outputs-out-dir lib
...
[Sandbox] Watching for file changes...
File written: lib/amplify_outputs.dart
Enter fullscreen mode Exit fullscreen mode

Set up backend resources

Here, we define resources on how to connect to Amazon Bedrock.

amplify/data/resource.ts

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

const schema = a.schema({
  Dummy: a
    .model({
      dummy: a.string(),
    })
    .authorization((allow) => [allow.owner()]),

  generateRecipe: a.generation({
    aiModel: a.ai.model('Claude 3 Haiku'),
    systemPrompt: 'You are a helpful assistant that generates recipes.',
  })
    .arguments({
      description: a.string(),
    })
    .returns(
      a.customType({
        name: a.string(),
        ingredients: a.string().array(),
        instructions: a.string(),
      })
    )
    .authorization((allow) => allow.authenticated()),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool',
  },
});
Enter fullscreen mode Exit fullscreen mode

generateRecipe is the name of the function called Flutter.

a.generation() is the function defined by the Amplify Library. It can describe a Gen-AI model, a system prompt, its arguments, and return types.

When you save the code above, Sandbox will automatically rerun. Please wait until you finish with Sandbox, open another terminal, and execute the generate command as shown below.

$ npx ampx generate graphql-client-code --format modelgen --model-target dart --out ./lib/models
Enter fullscreen mode Exit fullscreen mode

This allows you to use Model Types from the Flutter frontend.

Notice:
amplify/data/resource.ts must include at least one a.model().
The generate command will export incomplete Types if you do not write any a.model().

Implement the Flutter frontend

Implementation lib/main.dart like below.

import 'dart:convert';

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:flutter/material.dart';
import 'package:flutter_amplify_ai_kit/models/ModelProvider.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});
  final title = 'AI Kit Demo for Flutter';

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return Authenticator(
      child: MaterialApp(
        title: title,
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
          useMaterial3: true,
        ),
        builder: Authenticator.builder(),
        home: MyHomePage(title: title),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String requestRecipe = '';
  String responseRecipeName = '';
  List<String> responseRecipeIngredients = [];
  String responseRecipeInstructions = '';

  final TextEditingController _requestRecipeController =
      TextEditingController();

  @override
  void dispose() {
    _requestRecipeController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        spacing: 16,
        children: <Widget>[
          Row(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              SignOutButton(),
            ],
          ),
          TextField(
            controller: _requestRecipeController,
            decoration: InputDecoration(
              labelText: 'Input your request',
              border: OutlineInputBorder(),
            ),
            onChanged: (text) {
              safePrint("Current text: $text");
            },
          ),
          Card(
            child: Column(
              children: [
                ListTile(
                  title: Text('Recipe'),
                  subtitle: Text(responseRecipeName),
                ),
                const Divider(),
                ListTile(
                  title: Text('Ingredients'),
                  subtitle: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: responseRecipeIngredients.map((ingredient) {
                      return Text(ingredient);
                    }).toList(),
                  ),
                ),
                const Divider(),
                ListTile(
                  title: Text('Instructions'),
                  subtitle: Text(responseRecipeInstructions),
                ),
              ],
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _generateRecipe,
        tooltip: 'Generate Recipe',
        child: const Icon(Icons.send),
      ),
    );
  }

  _generateRecipe() async {
    try {
      const graphQLDocument = '''
  query GenerateRecipe(\$description: String!) {
    generateRecipe(description: \$description) {
      name
      ingredients
      instructions
    }
  }
''';

      if (_requestRecipeController.text.isEmpty) {
        return;
      }

      final generateRecipeRequest = GraphQLRequest<String>(
        document: graphQLDocument,
        variables: <String, String>{
          "description": _requestRecipeController.text,
        },
      );

      final response =
          await Amplify.API.query(request: generateRecipeRequest).response;
      safePrint(response);

      Map<String, dynamic> jsonMap = json.decode(response.data!);
      safePrint(jsonMap);
      GenerateRecipeReturnType generateRecipeResponse =
          GenerateRecipeReturnType.fromJson(jsonMap['generateRecipe']);
      safePrint(generateRecipeResponse);
      setState(() {
        responseRecipeName = generateRecipeResponse.name ?? '';
        responseRecipeIngredients = generateRecipeResponse.ingredients ?? [];
        responseRecipeInstructions = generateRecipeResponse.instructions ?? '';
      });
    } on ApiException catch (e) {
      safePrint('Query failed: $e');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

_generateRecipe() is the central part of the Amazon Bedrock call.

graphQLDocument is the GraphQL query to call generateRecipe previously defined in amplify/data/resource.ts.

Use Amplify.API.query() to connect to Amazon Bedrock via AWS AppSync.

Finally, we can use Amazon Bedrock from Flutter via the AWS Amplify AI Kit!

Check

Let's run and check this app.

$ flutter run -d chrome  
Enter fullscreen mode Exit fullscreen mode

Demo

Clean up

If you want to finish and delete Sandbox, you can just run the steps below.

$ npx ampx sandbox delete
? Are you sure you want to delete all the resources in your sandbox environment (This can't be undone)? (y/N) y
...
[Sandbox] Finished deleting.
$
Enter fullscreen mode Exit fullscreen mode

Top comments (0)