Page updated Feb 14, 2024

Mobile support

Amplify Gen 2 enables backend code sharing between web and mobile apps. The initial focus is on a TypeScript-first experience optimized for web developers. Mobile developers can also leverage the Gen 2 capabilities to build a unified backend for Android, iOS, Flutter and React Native apps. Expect ongoing improvements to streamline mobile workflows as we gather feedback from developers.

Prerequisites

Before you get started, make sure you have the following installed:

Build a mobile app

Here is how you can build a To Do application on each platform with CRUD operations:

You need to have Android Studio and SDK installed on your machine.

Open Android Studio. Select + Create New Project.

Shows the Android studio welcome window

In Select a Project Template, select Empty Activity or Empty Compose Activity. Press Next.

Shows Android studio new project window

  • Enter MyAmplifyApp in the Name field
  • Select either Java or Kotlin from the Language dropdown menu
  • Select API 24: Android 7.0 (Nougat) from the Minimum SDK dropdown menu
  • Press Finish

Shows Android studio configure project window

This guide will expect you to use Kotlin DSL for Gradle. If you are using Groovy DSL, you will need to make some changes to the Gradle files.

Create Amplify Project

The easiest way to get started with AWS Amplify is through npm with create-amplify command. You can run it from your base project directory.

npm create amplify ? Where should we create your project? (.) # press enter
1npm create amplify
2? Where should we create your project? (.) # press enter

Running this command will scaffold a lightweight Amplify project in your current project with the following files added:

├── amplify/ │ ├── auth/ │ │ └── resource.ts │ ├── data/ │ │ └── resource.ts │ ├── backend.ts │ └── package.json ├── node_modules/ ├── .gitignore ├── package-lock.json ├── package.json └── tsconfig.json
1├── amplify/
2│ ├── auth/
3│ │ └── resource.ts
4│ ├── data/
5│ │ └── resource.ts
6│ ├── backend.ts
7│ └── package.json
8├── node_modules/
9├── .gitignore
10├── package-lock.json
11├── package.json
12└── tsconfig.json

Running Local Development Environment

Amplify gen2 provides a new way to develop applications. Now you are able to run your application with a sandbox environment and generate the configuration files for your application. To run your application with a sandbox environment, you can run the following command:

Be sure to add a "raw" folder under app/src/main/res directory if it doesn't exist.

npx amplify sandbox --config-format=json-mobile --config-out-dir=app/src/main/res/raw
1npx amplify sandbox --config-format=json-mobile --config-out-dir=app/src/main/res/raw

Adding Authentication

After the Amplify creation process, you can see a resource.ts file in the amplify/auth folder. This file contains the configuration for the authentication resource. The base code will enable the authentication with the default configuration. You can change the configuration based on your needs. For more information about the configuration, you can check the documentation.

import { defineAuth } from '@aws-amplify/backend'; export const auth = defineAuth({ loginWith: { email: true } });
1import { defineAuth } from '@aws-amplify/backend';
2
3export const auth = defineAuth({
4 loginWith: {
5 email: true
6 }
7});

After you have configured the authentication resource, you can use the Amplify UI libraries to run your authentication flow. Amplify UI is a collection of accessible, themeable, performant ui components that can connect directly to the Amplify resources.

To use the Amplify UI libraries, you need to add the following dependencies to your app/build.gradle file:

Be sure to have compileSdk version as 34 or higher.
dependencies { implementation("androidx.compose.material3:material3:1.1.0") implementation("com.amplifyframework.ui:authenticator:1.0.1") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") }
1dependencies {
2 implementation("androidx.compose.material3:material3:1.1.0")
3 implementation("com.amplifyframework.ui:authenticator:1.0.1")
4 coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
5}

Afterwards create a MyAmplifyApp class that extends Application and add the following code:

import android.app.Application import android.util.Log import com.amplifyframework.AmplifyException import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin import com.amplifyframework.core.Amplify class MyAmplifyApp: Application() { override fun onCreate() { super.onCreate() try { Amplify.addPlugin(AWSCognitoAuthPlugin()) Amplify.configure(applicationContext) Log.i("MyAmplifyApp", "Initialized Amplify") } catch (error: AmplifyException) { Log.e("MyAmplifyApp", "Could not initialize Amplify", error) } } }
1import android.app.Application
2import android.util.Log
3import com.amplifyframework.AmplifyException
4import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin
5import com.amplifyframework.core.Amplify
6
7class MyAmplifyApp: Application() {
8 override fun onCreate() {
9 super.onCreate()
10
11 try {
12 Amplify.addPlugin(AWSCognitoAuthPlugin())
13 Amplify.configure(applicationContext)
14 Log.i("MyAmplifyApp", "Initialized Amplify")
15 } catch (error: AmplifyException) {
16 Log.e("MyAmplifyApp", "Could not initialize Amplify", error)
17 }
18 }
19}

Next call this class in your AndroidManifest.xml file:

<application android:name=".MyAmplifyApp" ... </application>
1<application
2 android:name=".MyAmplifyApp"
3 ...
4</application>

Lastly update your MainActivity.kt file to use the Amplify UI components:

import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.amplifyframework.core.Amplify import com.amplifyframework.ui.authenticator.ui.Authenticator import <your-package-name>.ui.theme.MyAmplifyAppTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyAmplifyAppTheme { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { Authenticator { state -> Column { Text( text = "Hello ${state.user.username}!", ) Button(onClick = { Amplify.Auth.signOut { } }) { Text(text = "Sign Out") } } } } } } } }
1import android.os.Bundle
2import androidx.activity.ComponentActivity
3import androidx.activity.compose.setContent
4import androidx.compose.foundation.layout.Column
5import androidx.compose.foundation.layout.fillMaxSize
6import androidx.compose.material3.Button
7import androidx.compose.material3.MaterialTheme
8import androidx.compose.material3.Surface
9import androidx.compose.material3.Text
10import androidx.compose.runtime.Composable
11import androidx.compose.ui.Modifier
12import androidx.compose.ui.tooling.preview.Preview
13import com.amplifyframework.core.Amplify
14import com.amplifyframework.ui.authenticator.ui.Authenticator
15import <your-package-name>.ui.theme.MyAmplifyAppTheme
16
17class MainActivity : ComponentActivity() {
18 override fun onCreate(savedInstanceState: Bundle?) {
19 super.onCreate(savedInstanceState)
20 setContent {
21 MyAmplifyAppTheme {
22 // A surface container using the 'background' color from the theme
23 Surface(
24 modifier = Modifier.fillMaxSize(),
25 color = MaterialTheme.colorScheme.background
26 ) {
27 Authenticator { state ->
28 Column {
29 Text(
30 text = "Hello ${state.user.username}!",
31 )
32 Button(onClick = {
33 Amplify.Auth.signOut { }
34 }) {
35 Text(text = "Sign Out")
36 }
37 }
38 }
39 }
40 }
41 }
42 }
43}

Now if you run the application on the Android emulator, you should see the authentication flow working.

Adding GraphQL API

After the Amplify creation process, you can see a resource.ts file in the amplify/data folder. This file contains the configuration for the GraphQL API resource.

The default code will create a Todo model with content and isDone property. The authorization rules below specify that owners, authenticated via your Auth resource can "create", "read", "update", and "delete" their own records.

import { type ClientSchema, a, defineData } from '@aws-amplify/backend'; const schema = a.schema({ Todo: a .model({ content: a.string(), isDone: a.boolean() }) .authorization([a.allow.owner()]) }); export type Schema = ClientSchema<typeof schema>; export const data = defineData({ schema, authorizationModes: { defaultAuthorizationMode: 'userPool' } });
1import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
2
3const schema = a.schema({
4 Todo: a
5 .model({
6 content: a.string(),
7 isDone: a.boolean()
8 })
9 .authorization([a.allow.owner()])
10});
11
12export type Schema = ClientSchema<typeof schema>;
13
14export const data = defineData({
15 schema,
16 authorizationModes: {
17 defaultAuthorizationMode: 'userPool'
18 }
19});

To generate the model classes out of GraphQL schema, you can run the following command:

npx amplify generate graphql-client-code --format=modelgen --model-target=java --out=app/src/main/java
1npx amplify generate graphql-client-code --format=modelgen --model-target=java --out=app/src/main/java

Now you can see that the model classes are generated under app/src/main/java/com/amplifyframework/datastore/generated/model folder.

The generated models are in Java but do not worry Java and Kotlin are interoperable. You can use the generated models in your Kotlin code.

For using GraphQL API, you need to add the following dependencies to your app/build.gradle file:

dependencies { implementation("com.amplifyframework:core:2.14.4") implementation("com.amplifyframework:aws-api:2.14.4") }
1dependencies {
2 implementation("com.amplifyframework:core:2.14.4")
3 implementation("com.amplifyframework:aws-api:2.14.4")
4}

Afterwards open the MyAmplifyApp class and add the following line before the configure call:

Amplify.addPlugin(AWSApiPlugin())
1Amplify.addPlugin(AWSApiPlugin())

Now it is time to update the UI code a bit. Update the MainActivity class with the following code:

class MainActivity : ComponentActivity() { private val todoList = mutableStateListOf<Todo>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyApplicationTheme { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { Authenticator { _ -> Scaffold( floatingActionButton = { FloatingActionButton( onClick = { val date = Date() val offsetMillis = TimeZone.getDefault().getOffset(date.time).toLong() val offsetSeconds = TimeUnit.MILLISECONDS.toSeconds(offsetMillis).toInt() val temporalDateTime = Temporal.DateTime(date, offsetSeconds) val todo = Todo.builder() .createdAt(temporalDateTime) .updatedAt(temporalDateTime) .content("My random todo ${System.currentTimeMillis()}") .isDone(false) .build() Amplify.API.mutate( ModelMutation.create(todo), { Log.i("MyAmplifyApp", "Added Todo with id: ${it.data.id}") todoList.add(todo) }, { Log.e("MyAmplifyApp", "Create failed", it) } ) }, ) { Icon(Icons.Filled.Add, "Add a random todo.") } } ) { Column(modifier = Modifier.padding(it)) { Button(onClick = { Amplify.Auth.signOut { } }) { Text(text = "Sign Out") } Text(text = "The list of items will come here.") } } } } } } } }
1class MainActivity : ComponentActivity() {
2
3 private val todoList = mutableStateListOf<Todo>()
4 override fun onCreate(savedInstanceState: Bundle?) {
5 super.onCreate(savedInstanceState)
6 setContent {
7 MyApplicationTheme {
8 // A surface container using the 'background' color from the theme
9 Surface(
10 modifier = Modifier.fillMaxSize(),
11 color = MaterialTheme.colorScheme.background
12 ) {
13 Authenticator { _ ->
14 Scaffold(
15 floatingActionButton = {
16 FloatingActionButton(
17 onClick = {
18 val date = Date()
19 val offsetMillis = TimeZone.getDefault().getOffset(date.time).toLong()
20 val offsetSeconds = TimeUnit.MILLISECONDS.toSeconds(offsetMillis).toInt()
21 val temporalDateTime = Temporal.DateTime(date, offsetSeconds)
22 val todo = Todo.builder()
23 .createdAt(temporalDateTime)
24 .updatedAt(temporalDateTime)
25 .content("My random todo ${System.currentTimeMillis()}")
26 .isDone(false)
27 .build()
28
29 Amplify.API.mutate(
30 ModelMutation.create(todo),
31 {
32 Log.i("MyAmplifyApp", "Added Todo with id: ${it.data.id}")
33 todoList.add(todo)
34 },
35 { Log.e("MyAmplifyApp", "Create failed", it) }
36 )
37 },
38 ) {
39 Icon(Icons.Filled.Add, "Add a random todo.")
40 }
41 }
42 ) {
43 Column(modifier = Modifier.padding(it)) {
44 Button(onClick = {
45 Amplify.Auth.signOut { }
46 }) {
47 Text(text = "Sign Out")
48 }
49 Text(text = "The list of items will come here.")
50 }
51 }
52 }
53 }
54 }
55 }
56 }
57}

The onClick function of the FloatingActionButton will create a random Todo item. Now it is time to add a logic to see the added items.

First let's add a TodoScreen composable function:

@Composable fun TodoScreen( todoList: SnapshotStateList<Todo>, onItemUpdated: (Todo) -> Unit, onItemDeleted: (Todo) -> Unit, ) { LazyColumn { todoList.forEach { todo -> item { Row(verticalAlignment = Alignment.CenterVertically) { Checkbox( checked = todo.isDone, onCheckedChange = { } ) Text(todo.content) } } } } }
1@Composable
2fun TodoScreen(
3 todoList: SnapshotStateList<Todo>,
4 onItemUpdated: (Todo) -> Unit,
5 onItemDeleted: (Todo) -> Unit,
6) {
7 LazyColumn {
8 todoList.forEach { todo ->
9 item {
10 Row(verticalAlignment = Alignment.CenterVertically) {
11 Checkbox(
12 checked = todo.isDone,
13 onCheckedChange = { }
14 )
15 Text(todo.content)
16 }
17 }
18 }
19 }
20}

Next let's update the MainActivity class to use the TodoScreen composable function. Update the Authenticator usage with the following code:

Authenticator(modifier = Modifier.padding(it)) { _ -> Column { Button(onClick = { Amplify.Auth.signOut { } }) { Text(text = "Sign Out") } if (todoList.isEmpty()) Text(text = "The list is empty.\nAdd some items by clicking the Floating Action Button.") else TodoScreen( todoList, onItemUpdated = { todo -> val foundItem = todoList.firstOrNull { it.id == todo.id } if (foundItem != null) { val index = todoList.indexOf(foundItem) todoList.removeAt(index) Log.i("updated", todo.toString()) todoList.add(index, todo) } }, ) { todo -> todoList.remove(todo) } } }
1Authenticator(modifier = Modifier.padding(it)) { _ ->
2 Column {
3 Button(onClick = {
4 Amplify.Auth.signOut { }
5 }) {
6 Text(text = "Sign Out")
7 }
8 if (todoList.isEmpty())
9 Text(text = "The list is empty.\nAdd some items by clicking the Floating Action Button.")
10 else
11 TodoScreen(
12 todoList,
13 onItemUpdated = { todo ->
14 val foundItem =
15 todoList.firstOrNull { it.id == todo.id }
16 if (foundItem != null) {
17 val index = todoList.indexOf(foundItem)
18 todoList.removeAt(index)
19 Log.i("updated", todo.toString())
20 todoList.add(index, todo)
21 }
22 },
23 ) { todo -> todoList.remove(todo) }
24 }
25}

Now add a todoList variable to the MainActivity class before the onCreate call and call the refreshItems function before the setContent call:

override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) refreshItems() ... }
1override fun onCreate(savedInstanceState: Bundle?) {
2 super.onCreate(savedInstanceState)
3 refreshItems()
4...
5}

Lastly create a function to fetch the items. Add the following code to the MainActivity class:

private fun refreshItems() { Amplify.API.query( ModelQuery.list(Todo::class.java), { response -> val items = response.data items.forEach { todo -> val foundItem = todoList.firstOrNull { it.id == todo.id } if (foundItem != null) { val index = todoList.indexOf(foundItem) todoList.removeAt(index) todoList.add(index, todo) } else { todoList.add(todo) } } Log.i("MyAmplifyApp", "Queried items: $items") }, { Log.e("MyAmplifyApp", "Query failure", it) } ) }
1private fun refreshItems() {
2 Amplify.API.query(
3 ModelQuery.list(Todo::class.java),
4 { response ->
5 val items = response.data
6 items.forEach { todo ->
7 val foundItem = todoList.firstOrNull { it.id == todo.id }
8 if (foundItem != null) {
9 val index = todoList.indexOf(foundItem)
10 todoList.removeAt(index)
11 todoList.add(index, todo)
12 } else {
13 todoList.add(todo)
14 }
15 }
16 Log.i("MyAmplifyApp", "Queried items: $items")
17 },
18 { Log.e("MyAmplifyApp", "Query failure", it) }
19 )
20}

Now let's update and delete the items. For update, add the following code to the onCheckedChange method of the Checkbox widget:

val newTodo = todo.copyOfBuilder().isDone(it).build() Amplify.API.mutate( ModelMutation.update(newTodo), { Log.i("MyAmplifyApp", "Updated Todo with id: ${todo.id}") onItemUpdated(newTodo) }, { Log.e("MyAmplifyApp", "Update failed") } )
1val newTodo = todo.copyOfBuilder().isDone(it).build()
2Amplify.API.mutate(
3 ModelMutation.update(newTodo),
4 {
5 Log.i("MyAmplifyApp", "Updated Todo with id: ${todo.id}")
6 onItemUpdated(newTodo)
7 },
8 { Log.e("MyAmplifyApp", "Update failed") }
9)

For deleting add a long click behavior with the Modifier.combinedClickable modifier. To add it, update the Row composable call in the TodoScreen composable function with the following code:

Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.combinedClickable( onClick = { val newTodo = todo.copyOfBuilder().isDone(!todo.isDone).build() Amplify.API.mutate( ModelMutation.update(newTodo), { Log.i("MyAmplifyApp", "Updated Todo with id: ${todo.id}") onItemUpdated(newTodo) }, { Log.e("MyAmplifyApp", "Update failed") } ) }, onLongClick = { Amplify.API.mutate( ModelMutation.delete(todo), { Log.i("MyAmplifyApp", "Deleted Todo with id: ${todo.id}") onItemDeleted(todo) }, { Log.e("MyAmplifyApp", "Delete failed") } ) }, ) )
1Row(
2 verticalAlignment = Alignment.CenterVertically,
3 modifier = Modifier.combinedClickable(
4 onClick = {
5 val newTodo = todo.copyOfBuilder().isDone(!todo.isDone).build()
6 Amplify.API.mutate(
7 ModelMutation.update(newTodo),
8 {
9 Log.i("MyAmplifyApp", "Updated Todo with id: ${todo.id}")
10 onItemUpdated(newTodo)
11 },
12 { Log.e("MyAmplifyApp", "Update failed") }
13 )
14 },
15 onLongClick = {
16 Amplify.API.mutate(
17 ModelMutation.delete(todo),
18 {
19 Log.i("MyAmplifyApp", "Deleted Todo with id: ${todo.id}")
20 onItemDeleted(todo)
21 },
22 { Log.e("MyAmplifyApp", "Delete failed") }
23 )
24 },
25 )
26)

With the click, we update the checkbox but with the long click we remove it. Now if you run the application you should see the following flow.

You can terminate the sandbox environment now to clean up the project.

Publishing changes to cloud

For publishing the changes to cloud, you need to create a remote git repository. For a detailed guide, you can follow the link here.

For using Flutter with Amplify Gen2, you need to have a Flutter version higher than 3.3.0 and setup the editor of you

You can follow the official documentation to install Flutter on your machine and check the editor documentation for setting up your editor.

Once you have installed Flutter, you can create a new Flutter project using the following command:

flutter create my_amplify_app
1flutter create my_amplify_app

Create Amplify Project

The easiest way to get started with AWS Amplify is through npm with create-amplify command. You can run it from your base project directory.

npm create amplify ? Where should we create your project? (.) # press enter
1npm create amplify
2? Where should we create your project? (.) # press enter

Running this command will scaffold a lightweight Amplify project in your current project with the following files added:

├── amplify/ │ ├── auth/ │ │ └── resource.ts │ ├── data/ │ │ └── resource.ts │ ├── backend.ts │ └── package.json ├── node_modules/ ├── .gitignore ├── package-lock.json ├── package.json └── tsconfig.json
1├── amplify/
2│ ├── auth/
3│ │ └── resource.ts
4│ ├── data/
5│ │ └── resource.ts
6│ ├── backend.ts
7│ └── package.json
8├── node_modules/
9├── .gitignore
10├── package-lock.json
11├── package.json
12└── tsconfig.json

Running Local Development Environment

Amplify gen2 provides a new way to develop applications. Now you are able to run your application with a sandbox environment and generate the configuration files for your application. To run your application with a sandbox environment, you can run the following command:

npx amplify sandbox --config-format=dart --config-out-dir=lib
1npx amplify sandbox --config-format=dart --config-out-dir=lib

Adding Authentication

After the Amplify creation process, you can see a resource.ts file in the amplify/auth folder. This file contains the configuration for the authentication resource. The base code will enable the authentication with the default configuration. You can change the configuration based on your needs. For more information about the configuration, you can check the documentation.

import { defineAuth } from '@aws-amplify/backend'; export const auth = defineAuth({ loginWith: { email: true } });
1import { defineAuth } from '@aws-amplify/backend';
2
3export const auth = defineAuth({
4 loginWith: {
5 email: true
6 }
7});

After you have configured the authentication resource, you can use the Amplify UI libraries to run your authentication flow. Amplify UI is a collection of accessible, themeable, performant ui components that can connect directly to the Amplify resources.

To use the Amplify UI libraries, you need to add the following dependencies to your pubspec.yaml file:

dependencies: amplify_flutter: ^1.0.0 amplify_auth_cognito: ^1.0.0 amplify_authenticator: ^1.0.0
1dependencies:
2 amplify_flutter: ^1.0.0
3 amplify_auth_cognito: ^1.0.0
4 amplify_authenticator: ^1.0.0

You will add:

  • amplify_flutter to connect your application with the Amplify resources.
  • amplify_auth_cognito to connect your application with the Amplify Cognito resources.
  • amplify_authenticator to use the Amplify UI components.

After adding the dependencies, you need to run the following command to install the dependencies:

flutter pub get
1flutter pub get

Lastly update your main.dart file to use the Amplify UI components:

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 'amplifyconfiguration.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.addPlugin(AmplifyAuthCognito()); 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 Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SignOutButton(), Text('TODO Application'), ], ), ), ), ), ); } }
1import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
2import 'package:amplify_authenticator/amplify_authenticator.dart';
3import 'package:amplify_flutter/amplify_flutter.dart';
4import 'package:flutter/material.dart';
5
6import 'amplifyconfiguration.dart';
7
8Future<void> main() async {
9 try {
10 WidgetsFlutterBinding.ensureInitialized();
11 await _configureAmplify();
12 runApp(const MyApp());
13 } on AmplifyException catch (e) {
14 runApp(Text("Error configuring Amplify: ${e.message}"));
15 }
16}
17
18Future<void> _configureAmplify() async {
19 try {
20 await Amplify.addPlugin(AmplifyAuthCognito());
21 await Amplify.configure(amplifyConfig);
22 safePrint('Successfully configured');
23 } on Exception catch (e) {
24 safePrint('Error configuring Amplify: $e');
25 }
26}
27
28class MyApp extends StatelessWidget {
29 const MyApp({super.key});
30
31 Widget build(BuildContext context) {
32 return Authenticator(
33 child: MaterialApp(
34 builder: Authenticator.builder(),
35 home: const Scaffold(
36 body: Center(
37 child: Column(
38 mainAxisAlignment: MainAxisAlignment.center,
39 children: [
40 SignOutButton(),
41 Text('TODO Application'),
42 ],
43 ),
44 ),
45 ),
46 ),
47 );
48 }
49}

The Authenticator widget provides an authentication flow according to the resources that has been configured. If you run the application now (on Flutter you can run your applications on Web, Desktop and Mobile), you can see the authentication flow working.

Adding Data

After the Amplify creation process, you can see a resource.ts file in the amplify/data folder. This file contains the configuration for the GraphQL API resource.

The default code will create a Todo model with content and isDone property. The authorization rules below specify that owners, authenticated via your Auth resource can "create", "read", "update", and "delete" their own records.

import { type ClientSchema, a, defineData } from '@aws-amplify/backend'; const schema = a.schema({ Todo: a .model({ content: a.string(), isDone: a.boolean() }) .authorization([a.allow.owner()]) }); export type Schema = ClientSchema<typeof schema>; export const data = defineData({ schema, authorizationModes: { defaultAuthorizationMode: 'userPool' } });
1import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
2
3const schema = a.schema({
4 Todo: a
5 .model({
6 content: a.string(),
7 isDone: a.boolean()
8 })
9 .authorization([a.allow.owner()])
10});
11
12export type Schema = ClientSchema<typeof schema>;
13
14export const data = defineData({
15 schema,
16 authorizationModes: {
17 defaultAuthorizationMode: 'userPool'
18 }
19});

To generate the model classes out of GraphQL schema, you can run the following command:

npx amplify generate graphql-client-code --format=modelgen --model-target=dart --out=lib/models
1npx amplify generate graphql-client-code --format=modelgen --model-target=dart --out=lib/models

This will generate dart models under lib/models folder.

For using GraphQL API, you need to add the following dependencies to your pubspec.yaml file:

dependencies: amplify_api: ^1.0.0
1dependencies:
2 amplify_api: ^1.0.0

You will add amplify_api to connect your application with the Amplify API.

After adding the dependencies, update the _configureAmplify method in your main.dart file to use the Amplify API:

Future<void> _configureAmplify() async { try { await Amplify.addPlugins( [ AmplifyAuthCognito(), AmplifyAPI(modelProvider: ModelProvider.instance), ], ); await Amplify.configure(amplifyConfig); safePrint('Successfully configured'); } on Exception catch (e) { safePrint('Error configuring Amplify: $e'); } }
1Future<void> _configureAmplify() async {
2 try {
3 await Amplify.addPlugins(
4 [
5 AmplifyAuthCognito(),
6 AmplifyAPI(modelProvider: ModelProvider.instance),
7 ],
8 );
9 await Amplify.configure(amplifyConfig);
10 safePrint('Successfully configured');
11 } on Exception catch (e) {
12 safePrint('Error configuring Amplify: $e');
13 }
14}

Next create a new widget called TodoScreen and add the following code:

class TodoScreen extends StatefulWidget { const TodoScreen({super.key}); @override State<TodoScreen> createState() => _TodoScreenState(); } class _TodoScreenState extends State<TodoScreen> { @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, createdAt: TemporalDateTime(DateTime.now()), updatedAt: TemporalDateTime(DateTime.now()), ); 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.'); } }, ), body: const Placeholder(), ); } }
1class TodoScreen extends StatefulWidget {
2 const TodoScreen({super.key});
3
4
5 State<TodoScreen> createState() => _TodoScreenState();
6}
7
8class _TodoScreenState extends State<TodoScreen> {
9
10 Widget build(BuildContext context) {
11 return Scaffold(
12 floatingActionButton: FloatingActionButton.extended(
13 label: const Text('Add Random Todo'),
14 onPressed: () async {
15 final newTodo = Todo(
16 id: uuid(),
17 content: "Random Todo ${DateTime.now().toIso8601String()}",
18 isDone: false,
19 createdAt: TemporalDateTime(DateTime.now()),
20 updatedAt: TemporalDateTime(DateTime.now()),
21 );
22 final request = ModelMutations.create(newTodo);
23 final response = await Amplify.API.mutate(request: request).response;
24 if (response.hasErrors) {
25 safePrint('Creating Todo failed.');
26 } else {
27 safePrint('Creating Todo successful.');
28 }
29 },
30 ),
31 body: const Placeholder(),
32 );
33 }
34}

This will create a random Todo every time a user clicks on the floating action button. You can see the ModelMutations.create method is used to create a new Todo.

And update the Text('TODO Application') line in your main.dart file to use the TodoScreen widget.

Next add a _todos list to add the results from the API and call the refresh function:

List<Todo> _todos = []; @override void initState() { super.initState(); _refreshTodos(); }
1List<Todo> _todos = [];
2
3
4void initState() {
5 super.initState();
6 _refreshTodos();
7}

and update the body with the following code:

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 { return false; }, child: CheckboxListTile.adaptive( value: todo.isDone, title: Text(todo.content!), onChanged: (isChecked) async { }, ), ); }, ),
1body: _todos.isEmpty == true
2 ? const Center(
3 child: Text(
4 "The list is empty.\nAdd some items by clicking the floating action button.",
5 textAlign: TextAlign.center,
6 ),
7 )
8 : ListView.builder(
9 itemCount: _todos.length,
10 itemBuilder: (context, index) {
11 final todo = _todos[index];
12 return Dismissible(
13 key: UniqueKey(),
14 confirmDismiss: (direction) async {
15 return false;
16 },
17 child: CheckboxListTile.adaptive(
18 value: todo.isDone,
19 title: Text(todo.content!),
20 onChanged: (isChecked) async { },
21 ),
22 );
23 },
24 ),

and create a new function called _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'); } }
1Future<void> _refreshTodos() async {
2 try {
3 final request = ModelQueries.list(Todo.classType);
4 final response = await Amplify.API.query(request: request).response;
5
6 final todos = response.data?.items;
7 if (response.hasErrors) {
8 safePrint('errors: ${response.errors}');
9 return;
10 }
11 setState(() {
12 _todos = todos!.whereType<Todo>().toList();
13 });
14 } on ApiException catch (e) {
15 safePrint('Query failed: $e');
16 }
17}

Now let's add a update and delete functionality.

For update, add the following code to the onChanged method of the CheckboxListTile.adaptive widget:

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(); }
1final request = ModelMutations.update(
2 todo.copyWith(isDone: isChecked!),
3);
4final response =
5 await Amplify.API.mutate(request: request).response;
6if (response.hasErrors) {
7 safePrint('Updating Todo failed. ${response.errors}');
8} else {
9 safePrint('Updating Todo successful.');
10 await _refreshTodos();
11}

This will call the ModelMutations.update method to update the Todo with a copied/updated version of the todo item. So now the checkbox will get an update as well.

For delete functionality, add the following code to the confirmDismiss method of the Dismissible widget:

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;
1if (direction == DismissDirection.endToStart) {
2 final request = ModelMutations.delete(todo);
3 final response =
4 await Amplify.API.mutate(request: request).response;
5 if (response.hasErrors) {
6 safePrint('Updating Todo failed. ${response.errors}');
7 } else {
8 safePrint('Updating Todo successful.');
9 await _refreshTodos();
10 return true;
11 }
12}
13return false;

This will delete the Todo item when the user swipes the item from right to left. Now if you run the application you should see the following flow.

You can terminate the sandbox environment now to clean up the project.

Publishing changes to cloud

For publishing the changes to cloud, you need to create a remote git repository. For a detailed guide, you can follow the link here.

You need to have Xcode and Developer Tooling installed on your machine.

Open Xcode and select Create New Project...

Shows the Xcode starter video to start project

In the next step select the App template under iOS. Click on next.

Shows the template of apps for iOS

Next steps are:

  • Adding a Product Name (e.g. MyAmplifyApp)
  • Select a Team (e.g. None)
  • Select a Organization Identifier (e.g. com.example)
  • Select SwiftUI an Interface.
  • Press Next

Shows the project details dialog

Now you should have your project created.

Shows the base project for SwiftUI

Create Amplify Project

The easiest way to get started with AWS Amplify is through npm with create-amplify command. You can run it from your base project directory.

npm create amplify ? Where should we create your project? (.) # press enter
1npm create amplify
2? Where should we create your project? (.) # press enter

Running this command will scaffold a lightweight Amplify project in your current project with the following files added:

├── amplify/ │ ├── auth/ │ │ └── resource.ts │ ├── data/ │ │ └── resource.ts │ ├── backend.ts │ └── package.json ├── node_modules/ ├── .gitignore ├── package-lock.json ├── package.json └── tsconfig.json
1├── amplify/
2│ ├── auth/
3│ │ └── resource.ts
4│ ├── data/
5│ │ └── resource.ts
6│ ├── backend.ts
7│ └── package.json
8├── node_modules/
9├── .gitignore
10├── package-lock.json
11├── package.json
12└── tsconfig.json

Running Local Development Environment

Amplify gen2 provides a new way to develop applications. Now you are able to run your application with a sandbox environment and generate the configuration files for your application. To run your application with a sandbox environment, you can run the following command:

npx amplify sandbox --config-format=json-mobile
1npx amplify sandbox --config-format=json-mobile

Once the sandbox environment is running, you would also generate the configuration files for your application. However, Xcode won't be able to recognize them. For recognizing the files, you need to drag and drop the generated files to your project.

Adding Authentication

After the Amplify creation process, you can see a resource.ts file in the amplify/auth folder. This file contains the configuration for the authentication resource. The base code will enable the authentication with the default configuration. You can change the configuration based on your needs. For more information about the configuration, you can check the documentation.

import { defineAuth } from '@aws-amplify/backend'; export const auth = defineAuth({ loginWith: { email: true } });
1import { defineAuth } from '@aws-amplify/backend';
2
3export const auth = defineAuth({
4 loginWith: {
5 email: true
6 }
7});

After you have configured the authentication resource, you can use the Amplify UI libraries to run your authentication flow. Amplify UI is a collection of accessible, themeable, performant ui components that can connect directly to the Amplify resources.

Open your project in Xcode and select File > Add Packages... and add the following dependencies:

Shows the Amplify library for Swift

  • Amplify Library for Swift: Enter its GitHub URL (https://github.com/aws-amplify/amplify-swift), select Up to Next Major Version and click Add Package Dependencies... and select the following libraries:

    • Amplify
    • AWSCognitoAuthPlugin

Shows the Amplify library for Swift

Shows the Amplify library for Swift

Now update the MyAmplifyAppApp class with the following code:

import Amplify import Authenticator import AWSCognitoAuthPlugin import SwiftUI @main struct MyApp: App { init() { do { try Amplify.add(plugin: AWSCognitoAuthPlugin()) try Amplify.configure() } catch { print("Unable to configure Amplify \(error)") } } var body: some Scene { WindowGroup { Authenticator { state in VStack { Button("Sign out") { Task { await state.signOut() } } Spacer() Button(action: { Task { await createTodo() await listTodos() } }) { HStack { Text("Add a New Todo") Image(systemName: "plus") } } .accessibilityLabel("New Todo") } } } } }
1import Amplify
2import Authenticator
3import AWSCognitoAuthPlugin
4import SwiftUI
5
6@main
7struct MyApp: App {
8 init() {
9 do {
10 try Amplify.add(plugin: AWSCognitoAuthPlugin())
11 try Amplify.configure()
12 } catch {
13 print("Unable to configure Amplify \(error)")
14 }
15 }
16
17 var body: some Scene {
18 WindowGroup {
19 Authenticator { state in
20 VStack {
21 Button("Sign out") {
22 Task {
23 await state.signOut()
24 }
25 }
26 Spacer()
27 Button(action: {
28 Task {
29 await createTodo()
30 await listTodos()
31 }
32 }) {
33 HStack {
34 Text("Add a New Todo")
35 Image(systemName: "plus")
36 }
37 }
38 .accessibilityLabel("New Todo")
39 }
40 }
41 }
42 }
43}

This will add the authentication flow by using the Authenticator component and add a sign out button with a create todo button.

If you run the application now, you can see that the authentication flow is working.

Adding Data

After the Amplify creation process, you can see a resource.ts file in the amplify/data folder. This file contains the configuration for the GraphQL API resource.

The default code will create a Todo model with content and isDone property. The authorization rules below specify that owners, authenticated via your Auth resource can "create", "read", "update", and "delete" their own records.

import { type ClientSchema, a, defineData } from '@aws-amplify/backend'; const schema = a.schema({ Todo: a .model({ content: a.string(), isDone: a.boolean() }) .authorization([a.allow.owner()]) }); export type Schema = ClientSchema<typeof schema>; export const data = defineData({ schema, authorizationModes: { defaultAuthorizationMode: 'userPool' } });
1import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
2
3const schema = a.schema({
4 Todo: a
5 .model({
6 content: a.string(),
7 isDone: a.boolean()
8 })
9 .authorization([a.allow.owner()])
10});
11
12export type Schema = ClientSchema<typeof schema>;
13
14export const data = defineData({
15 schema,
16 authorizationModes: {
17 defaultAuthorizationMode: 'userPool'
18 }
19});

To generate the model classes out of GraphQL schema, you can run the following command:

npx amplify generate graphql-client-code --format=modelgen
1npx amplify generate graphql-client-code --format=modelgen

Move the generated files to your project. You can do this by dragging and dropping the files to your project.

Shows the drag and drop phase

Once you are done, add the API dependencies to your project. Select File > Add Package Dependencies... and add the AWSAPIPlugin.

Shows the Amplify API library for Swift selected

Next, update the init part of your MyAmplifyAppApp.swift file with the following code:

init() { do { try Amplify.add(plugin: AWSCognitoAuthPlugin()) try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels())) try Amplify.configure() } catch { print("Unable to configure Amplify \(error)") } }
1init() {
2 do {
3 try Amplify.add(plugin: AWSCognitoAuthPlugin())
4 try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels()))
5 try Amplify.configure()
6 } catch {
7 print("Unable to configure Amplify \(error)")
8 }
9}

Now it is time to update the UI code a bit. Create a createTodo function in the MyAmplifyAppApp.swift file with the following code:

func createTodo() async { let creationTime = Temporal.DateTime.now() let todo = Todo( content: "Random Todo \(creationTime)", isDone: false, createdAt: creationTime, updatedAt: creationTime ) do { let result = try await Amplify.API.mutate(request: .create(todo)) switch result { case .success(let todo): print("Successfully created todo: \(todo)") case .failure(let error): print("Got failed result with \(error.errorDescription)") } } catch let error as APIError { print("Failed to create todo: ", error) } catch { print("Unexpected error: \(error)") } }
1func createTodo() async {
2 let creationTime = Temporal.DateTime.now()
3 let todo = Todo(
4 content: "Random Todo \(creationTime)",
5 isDone: false,
6 createdAt: creationTime,
7 updatedAt: creationTime
8 )
9 do {
10 let result = try await Amplify.API.mutate(request: .create(todo))
11 switch result {
12 case .success(let todo):
13 print("Successfully created todo: \(todo)")
14 case .failure(let error):
15 print("Got failed result with \(error.errorDescription)")
16 }
17 } catch let error as APIError {
18 print("Failed to create todo: ", error)
19 } catch {
20 print("Unexpected error: \(error)")
21 }
22}

The code above will create a random todo with the current time.

Next create a listTodos function in the MyAmplifyAppApp.swift file with the following code to have the logic of listing the items:

func listTodos() async { let request = GraphQLRequest<Todo>.list(Todo.self) do { let result = try await Amplify.API.query(request: request) switch result { case .success(let todos): self.todos = todos.elements print("Successfully retrieved list of todos: \(todos)") case .failure(let error): print("Got failed result with \(error.errorDescription)") } } catch let error as APIError { print("Failed to query list of todos: ", error) } catch { print("Unexpected error: \(error)") } }
1func listTodos() async {
2 let request = GraphQLRequest<Todo>.list(Todo.self)
3 do {
4 let result = try await Amplify.API.query(request: request)
5 switch result {
6 case .success(let todos):
7 self.todos = todos.elements
8 print("Successfully retrieved list of todos: \(todos)")
9 case .failure(let error):
10 print("Got failed result with \(error.errorDescription)")
11 }
12 } catch let error as APIError {
13 print("Failed to query list of todos: ", error)
14 } catch {
15 print("Unexpected error: \(error)")
16 }
17}

This will assign the value of the fetched todos into a State object. Be sure to create it before the body property:

@State var todos: [Todo] = []
1@State var todos: [Todo] = []

Now let's update the UI code to show the todos. Update the VStack in the MyAmplifyAppApp.swift file with the following code:

VStack { Button("Sign out") { Task { await state.signOut() } } List(todos, id: \.id) { todo in Text(todo.content!) } Button(action: { Task { await createTodo() await listTodos() } }) { HStack { Text("Add a New Todo") Image(systemName: "plus") } } .accessibilityLabel("New Todo") }.task { await listTodos() }
1VStack {
2 Button("Sign out") {
3 Task {
4 await state.signOut()
5 }
6 }
7 List(todos, id: \.id) { todo in
8 Text(todo.content!)
9 }
10 Button(action: {
11 Task {
12 await createTodo()
13 await listTodos()
14 }
15 }) {
16 HStack {
17 Text("Add a New Todo")
18 Image(systemName: "plus")
19 }
20 }
21 .accessibilityLabel("New Todo")
22}.task {
23 await listTodos()
24}

Throughout the Swift implementation, the async/await pattern has been used and for using it easily, we take advantage of the Task structure. For more information about the Task structure, you can check the documentation.

The code above will fetch the todos once the VStack is shown. It will also create a todo and update the todo list each time a todo is created.

Next step is to update and delete the todos. For that, create updateTodo and deleteTodo functions in the MyAmplifyAppApp.swift file with the following code:

func deleteTodo(todo: Todo) async { do { let result = try await Amplify.API.mutate(request: .delete(todo)) switch result { case .success(let todo): print("Successfully deleted todo: \(todo)") case .failure(let error): print("Got failed result with \(error.errorDescription)") } } catch let error as APIError { print("Failed to deleted todo: ", error) } catch { print("Unexpected error: \(error)") } } func updateTodo(todo: Todo) async { do { let result = try await Amplify.API.mutate(request: .update(todo)) switch result { case .success(let todo): print("Successfully updated todo: \(todo)") case .failure(let error): print("Got failed result with \(error.errorDescription)") } } catch let error as APIError { print("Failed to updated todo: ", error) } catch { print("Unexpected error: \(error)") } }
1func deleteTodo(todo: Todo) async {
2 do {
3 let result = try await Amplify.API.mutate(request: .delete(todo))
4 switch result {
5 case .success(let todo):
6 print("Successfully deleted todo: \(todo)")
7 case .failure(let error):
8 print("Got failed result with \(error.errorDescription)")
9 }
10 } catch let error as APIError {
11 print("Failed to deleted todo: ", error)
12 } catch {
13 print("Unexpected error: \(error)")
14 }
15}
16
17func updateTodo(todo: Todo) async {
18 do {
19 let result = try await Amplify.API.mutate(request: .update(todo))
20 switch result {
21 case .success(let todo):
22 print("Successfully updated todo: \(todo)")
23 case .failure(let error):
24 print("Got failed result with \(error.errorDescription)")
25 }
26 } catch let error as APIError {
27 print("Failed to updated todo: ", error)
28 } catch {
29 print("Unexpected error: \(error)")
30 }
31}

Lastly, update the List in the MyAmplifyAppApp.swift file with the following code:

List(todos, id: \.id) { todo in @State var isToggled = todo.isDone! Toggle(isOn: $isToggled ) { Text(todo.content!) }.onTapGesture { var updatedTodo = todos.first {$0.id == todo.id}! updatedTodo.isDone = !todo.isDone! Task { await updateTodo(todo: updatedTodo) await listTodos() } } .onChange(of: isToggled) { oldValue, newValue in var updatedTodo = todos.first {$0.id == todo.id}! updatedTodo.isDone = newValue Task { await updateTodo(todo: updatedTodo) await listTodos() } } .toggleStyle(.switch) .onLongPressGesture { Task { await deleteTodo(todo: todo) await listTodos() } } }
1List(todos, id: \.id) { todo in
2 @State var isToggled = todo.isDone!
3 Toggle(isOn: $isToggled
4 ) {
5 Text(todo.content!)
6 }.onTapGesture {
7 var updatedTodo = todos.first {$0.id == todo.id}!
8 updatedTodo.isDone = !todo.isDone!
9 Task {
10 await updateTodo(todo: updatedTodo)
11 await listTodos()
12 }
13 }
14 .onChange(of: isToggled) { oldValue, newValue in
15 var updatedTodo = todos.first {$0.id == todo.id}!
16 updatedTodo.isDone = newValue
17 Task {
18 await updateTodo(todo: updatedTodo)
19 await listTodos()
20 }
21 }
22 .toggleStyle(.switch)
23 .onLongPressGesture {
24 Task {
25 await deleteTodo(todo: todo)
26 await listTodos()
27 }
28 }
29}

This will update the UI to show a toggle to update the todo and a long press gesture to delete the todo. Now if you run the application you should see the following flow.

You can terminate the sandbox environment now to clean up the project.

Publishing changes to cloud

For publishing the changes to cloud, you need to create a remote git repository. For a detailed guide, you can follow the link here.