Coder Social home page Coder Social logo

aws-amplify / amplify-android Goto Github PK

View Code? Open in Web Editor NEW
229.0 46.0 107.0 62.35 MB

The fastest and easiest way to use AWS from your Android app.

Home Page: https://docs.amplify.aws/lib/q/platform/android/

License: Apache License 2.0

Java 60.79% Shell 0.32% Groovy 0.14% Python 0.04% Kotlin 38.70% Ruby 0.02%
android amplify aws aws-amplify

amplify-android's Introduction

Amplify for Android

AWS Amplify

DiscordChat GitHub release Maven Central

The Amplify Android library is AWS' preferred mechanism for interacting with AWS services from an Android device.

The library provides a high-level interface to perform different categories of cloud operations. Each category may be fulfilled by a plugin, which you configure during setup.

The default plugins that we provide are designed to facilitate interaction with Amazon Web Services (AWS). But, the Amplify Framework is designed to be extensible to any other backend or service.

To familiarize yourself with Amplify, checkout our Getting Started Guide.

Categories

Category AWS Provider Description
Authentication Cognito Building blocks to create auth experiences
Storage S3 Manages content in public, protected, private storage buckets
DataStore AppSync Programming model for shared and distributed data, with simple online/offline synchronization
API (GraphQL) AppSync Interact with your GraphQL or AppSync endpoint
API (REST) API Gateway Sigv4 signing and AWS auth for API Gateway and other REST endpoints
Analytics Pinpoint Collect Analytics data for your app including tracking user sessions
Geo Location Add maps to your app with APIs and map UI components
Predictions Various* Connect your app with machine learning services like NLP, computer vision, TTS, and more.
Push Notifications Pinpoint Segment users, trigger push notifications, and record metrics

* Predictions utilizes a range of Amazon's Machine Learning services, including: Amazon Comprehend, Amazon Polly, Amazon Rekognition, Amazon Textract, and Amazon Translate.

All services and features not listed above are supported via the Kotlin SDK or if supported by a category can be accessed via the Escape Hatch like below:

Kotlin

val s3StoragePlugin = Amplify.Storage.getPlugin("awsS3StoragePlugin")
val s3Client = s3StoragePlugin.escapeHatch as S3Client

Java

AWSS3StoragePlugin plugin = (AWSS3StoragePlugin) Amplify.Storage.getPlugin("awsS3StoragePlugin");
S3Client s3Client = plugin.getEscapeHatch();

Platform Support

The Amplify Framework supports Android API level 24 (Android 7.0) and above.

Using Amplify from Your App

For step-by-step setup instructions, checkout our Project Setup guide.

Specifying Gradle Dependencies

To begin, include Amplify from your app module's build.gradle dependencies section:

dependencies {
    // Only specify modules that provide functionality your app will use
    implementation 'com.amplifyframework:aws-analytics-pinpoint:2.16.1'
    implementation 'com.amplifyframework:aws-api:2.16.1'
    implementation 'com.amplifyframework:aws-auth-cognito:2.16.1'
    implementation 'com.amplifyframework:aws-datastore:2.16.1'
    implementation 'com.amplifyframework:aws-predictions:2.16.1'
    implementation 'com.amplifyframework:aws-storage-s3:2.16.1'
    implementation 'com.amplifyframework:aws-geo-location:2.16.1'
    implementation 'com.amplifyframework:aws-push-notifications-pinpoint:2.16.1'
}

Java 8 Requirement

Amplify Android requires Java 8 features. Please add a compileOptions block inside your app's build.gradle, as below:

android {
    compileOptions {
        coreLibraryDesugaringEnabled true
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_11
    }
}

In the same file, add core library desugaring in your dependencies block:

dependencies {
    // Add this line
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
}

Kotlin & Rx Support

Amplify's default interface renders results through async callbacks. We also provide optional, adapter APIs which better integrate with RxJava and Kotlin:

Semantic versioning

We follow semantic versioning for our releases.

Semantic versioning and enumeration cases

When Amplify adds a new a new enumeration class entry or sealed class subtype, we will publish a new minor version of the library.

Applications that use a when expression to evaluate all members of an enumerated type can add an else branch to prevent new cases from causing compile warnings or errors.

License

This library is licensed under the Apache 2.0 License.

Report a Bug

Open Bugs Open Questions Feature Requests Closed Issues

We appreciate your feedback – comments, questions, and bug reports. Please submit a GitHub issue, and we'll get back to you.

Contribute to the Project

We welcome any and all contributions from the community! Make sure you read through our Contribution Guidelines before submitting any PR's. Thanks! ♥️

amplify-android's People

Contributors

ankpshah avatar awsmobilesdk avatar banji180 avatar changxu0306 avatar dengdan154 avatar desokroshan avatar div5yesh avatar drochetti avatar eeatonaws avatar github-actions[bot] avatar gpanshu avatar huisf avatar jamesonwilliams avatar jordan-nelson avatar jpeddicord avatar jpignata avatar manueliglesias avatar mattcreaser avatar mikepschneider avatar mikschn-aws avatar poojamat avatar raphkim avatar richardmcclellan avatar rjuliano avatar royjit avatar sdhuka avatar sktimalsina avatar tjleing avatar treksoft avatar tylerjroach avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

amplify-android's Issues

Distribution infrastructure

Create the maven pom.xml files for each library and an overall pom.xml for all the AWS plugins and amplify-core.

Amplify.API.mutate UPDATE and DELETE does not modify DynamoDB database except for Version and Last Updated fields

Describe the bug
When I followed the Android getting started guide for AWS Amplify located here, I am able to create and query my DynamoDB database. However, when I try to update the object with the following code below, the version number and time last updated changes but the content of the object does not change. I am also not able to delete objects from the database.

Amplify.API.mutate(
            Store.builder().content("howdy").id("c29cd2ab-87cf-4dcb-a488-5c049fc0a3e4").build() ,
            MutationType.UPDATE,
            new ResultListener<GraphQLResponse<Store>>() {
                @Override
                public void onResult(GraphQLResponse<Store> response) {
                    Log.i("ApiQuickStart", "Deleted: " + response.getData().getContent());
                }
                @Override
                public void onError(Throwable throwable) {
                    Log.e("ApiQuickStart", throwable.getMessage());
                }
            }
    );

To Reproduce
Steps to reproduce the behavior:

  1. Follow the tutorial located here except for the change in step 2

  2. For graphql schema, I changed mine to reflect the following:
    type Product @model {
    id: ID!
    title: String!
    quantity: String
    }
    type Store @model {
    id: ID!
    content: String!
    products: [Product]
    }

  3. Add code to update a store object after creating an object and check the contents of the DynamoDB database after.

    Amplify.API.mutate(
    Store.builder().content("howdy").id("c29cd2ab-87cf-4dcb-a488-5c049fc0a3e4").build() ,
    MutationType.UPDATE,
    new ResultListener<GraphQLResponse>() {
    @OverRide
    public void onResult(GraphQLResponse response) {
    Log.i("ApiQuickStart", "Deleted: " + response.getData().getContent());
    }
    @OverRide
    public void onError(Throwable throwable) {
    Log.e("ApiQuickStart", throwable.getMessage());
    }
    }
    );

Expected behavior
I expect that when I update a store content from "Luckys" to "howdy" as shown above, the content should change to howdy. Instead, just the version and last updated time changes.

  • Device: Android Studio Pixel 3 emulator
  • OS: 10.14.6

Delete user account from AWS Cognito using the Amplify Android SDK

Which AWS Services is the feature request for?
Cognito

Is your feature request related to a problem? Please describe.
Using AWSMobileClient in an Android app allows to do pretty much anything, except deleting a user. A user cannot delete his account by himself from AWS Cognito only using AWSMobileClient.

Describe the solution you'd like
Is it possible to get a function that allows users to delete their accounts from AWS Cognito, using a command like AWSMobileClient.getInstance().deleteUser(...)?

Environment
AWS SDK for Android 2.14.1

Tasks in Core

  1. Refactor the Core Package to be internal and move Amplify class to upper level. See feedback for more details.

  2. Introduce reset() method for testing. See feedback for more details.

  3. Introduce a Task Manager: #12 (comment)

  4. Integrate AmplifyOperation with Hub.

  5. Refactor EscapeHatch as a separate interface and let each plugin choose to optionally implement them or not. For example, API GraphQL Plugin for AppSync and Amplify DataStore Plugin do not have an escape hatch mechanism.

  6. Add Auto Unsubscribe in Hub when the operation reaches a terminal state (completed/failed)

Update error message on attempt to delete not-present item

A model is saved, and then deleted.

A subsequent attempt is made to delete that same (already gone) model.

Expected: the error message says something like "failed to delete; model not found."

Actual: a generic message about a predicate failure.

2020-03-03 13:51:57.483 10975-11025/com.amplifyframework.ddsa I/MainActivity: Saved a post:Post{
      title=Title 6f56fc9d-1d63-4b99-a129-2ede1ff5c95e
      id=a08f9fd6-a26c-4dd4-98a6-41c533f769dd
      rating=1
      status=ACTIVE
    }
2020-03-03 13:52:03.266 10975-11025/com.amplifyframework.ddsa I/MainActivity: Deleted Post{
      title=Title 6f56fc9d-1d63-4b99-a129-2ede1ff5c95e
      id=a08f9fd6-a26c-4dd4-98a6-41c533f769dd
      rating=1
      status=ACTIVE
    }
2020-03-03 13:52:12.887 10975-11025/com.amplifyframework.ddsa I/MainActivity: com.amplifyframework.datastore.DataStoreException: Predicate condition not met.
        at com.amplifyframework.datastore.storage.sqlite.SQLiteStorageAdapter.lambda$delete$5(SourceFile:45)
        at com.amplifyframework.datastore.storage.sqlite.SQLiteStorageAdapter$$Lambda$4.run(Unknown Source:12)
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:462)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)
2020-03-03 13:53:02.408 10975-10986/com.amplifyframework.ddsa I/yframework.dds: System.exit called, status: 1
2020-03-03 13:53:02.408 10975-10986/com.amplifyframework.ddsa I/AndroidRuntime: VM exiting with result code 1, cleanup skipped.

Subscription is not working with Cognito User Pools

Amplify.API.subscribe works pretty well with API Key, but not with Cognito User Pools.
I changed my amplifyconfiguration.json like below to make use of Cognito User Pools.

{
  "UserAgent": "aws-amplify-cli/2.0",
  "Version": "1.0",
  "api": {
    "plugins": {
      "awsAPIPlugin": {
        "amplifyDatasource": {
          "endpointType": "GraphQL",
          "endpoint": "https://xxx.appsync-api.ap-northeast-1.amazonaws.com/graphql",
          "region": "ap-northeast-1",
          "authorizationType": "AMAZON_COGNITO_USER_POOLS",
        }
      }
    }
  }
}

And I started to get the error like this and subscription started not working.

2019-12-29 08:40:07.705 4492-4492/us.shiroyama.amplifyforandroid E/MainActivity: Subscription not acknowledged.
    com.amplifyframework.api.ApiException: Subscription not acknowledged.
        at com.amplifyframework.api.aws.SubscriptionEndpoint$Subscription.awaitSubscriptionReady(SubscriptionEndpoint.java:369)
        at com.amplifyframework.api.aws.SubscriptionEndpoint.requestSubscription(SubscriptionEndpoint.java:120)
        at com.amplifyframework.api.aws.SubscriptionOperation.start(SubscriptionOperation.java:84)
        at com.amplifyframework.api.aws.AWSApiPlugin.subscribe(AWSApiPlugin.java:445)
        at com.amplifyframework.api.aws.AWSApiPlugin.subscribe(AWSApiPlugin.java:404)
        at com.amplifyframework.api.aws.AWSApiPlugin.subscribe(AWSApiPlugin.java:376)
        at com.amplifyframework.api.ApiCategory.subscribe(ApiCategory.java:197)
        at us.shiroyama.amplifyforandroid.MainActivity.subscribe(MainActivity.kt:277)
        at us.shiroyama.amplifyforandroid.MainActivity.onStart(MainActivity.kt:91)
        at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1391)
        at android.app.Activity.performStart(Activity.java:7157)
        at android.app.ActivityThread.handleStartActivity(ActivityThread.java:2937)
        at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:180)
        at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:165)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:142)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6669)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

I tried to look into the source code, and figured out SubscriptionEndpoint#requestSubscription() timed out because it failed to make a valid WebSocket connection. It seemed like the class SubscriptionAuthorizationHeader in charge of creating request header only supported API key for now.

final class SubscriptionAuthorizationHeader {
    static JSONObject from(ApiConfiguration apiConfiguration) throws ApiException {
        final String host = Uri.parse(apiConfiguration.getEndpoint()).getHost();
        final String apiKey = apiConfiguration.getApiKey();
        try {
            return new JSONObject()
                .put("host", host)
                .put("x-amz-date", Iso8601Timestamp.now())
                .put("x-api-key", apiKey);

I tied to add apiKey in amplifyconfiguration.json as well, but this workaround seemed not working because the class AWSApiPluginConfigurationReader only allowed this configuration when the authorizationType equals AuthorizationType.API_KEY.

    private static AWSApiPluginConfiguration parseConfigurationJson(JSONObject configurationJson)
            throws ApiException {
                ...
                if (AuthorizationType.API_KEY.equals(authorizationType)) {
                    apiConfigBuilder.apiKey(apiSpec.getString(ConfigKey.API_KEY.key()));
                }
    }

Is there any workaround with this issue? Are you going to support subscription with Cognito User Pools? Thanks.

GsonVariablesSerializer should support ISO 8601 format for Date type

My GraphQL schema uses createdAt: AWSDateTime! type, and the auto-generated POJO class has a java.util.Date type for createdAt field.
When Amplify Android try to make a GraphQL query, this POJO is converted to a query string with using com.amplifyframework.api.aws.GsonVariablesSerializer by default which converts java.util.Date to yyyy-MM-dd format transparently.

public final class GsonVariablesSerializer implements GraphQLRequest.VariablesSerializer {
    @Override
    public String serialize(Map<String, Object> variables) {
        return new GsonBuilder()
                .registerTypeAdapter(Date.class, new DateSerializer())
                .create()
                .toJson(variables);
    }

    class DateSerializer implements JsonSerializer<Date> {
        public JsonElement serialize(Date date, Type typeOfSrc, JsonSerializationContext context) {
            DateFormat df = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
            return new JsonPrimitive(df.format(new Date()));
        }
    }
}

I believe AWSDateTime is ISO 8601 compliant according to the document, so this should be "yyyy-MM-dd'T'HH:mm:ssXXX" in order to keep the degree of precision.
https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html

To work around this, I know I can make GraphQLRequest manually as follows:

// make your own Serializer
class GsonVariablesSerializer : VariablesSerializer {
    override fun serialize(variables: Map<String, Any>): String {
        return GsonBuilder()
            .registerTypeAdapter(
                Date::class.java,
                DateSerializer()
            )
            .create()
            .toJson(variables)
    }

    internal inner class DateSerializer : JsonSerializer<Date?> {
        override fun serialize(
            date: Date?,
            typeOfSrc: Type,
            context: JsonSerializationContext
        ): JsonElement {
            val df: DateFormat = SimpleDateFormat(
                "yyyy-MM-dd'T'HH:mm:ssXXX",
                Locale.getDefault()
            )
            return JsonPrimitive(df.format(Date()))
        }
    }
}

// make manual request
val query = """
    mutation CreateMessage(
      ${'$'}id: ID!
      ${'$'}content: String!
      ${'$'}roomId: String!
      ${'$'}username: String!
      ${'$'}createdAt: AWSDateTime!
    ) {
      createMessage(input: {
        id: ${'$'}id
        content: ${'$'}content
        roomId: ${'$'}roomId
        username: ${'$'}username
        createdAt: ${'$'}createdAt
      }) {
        id
        content
        roomId
        username
        createdAt
      }
    }
    """.trimIndent()
val variables: Map<String, Any> = mutableMapOf(
    "id" to id,
    "content" to content,
    "roomId" to roomId,
    "username" to username,
    "createdAt" to createdAt
)
val request =
    GraphQLRequest(query, variables, Message::class.java, GsonVariablesSerializer())

Amplify.API.mutate(
    request,
    object :
        ResultListener<GraphQLResponse<Message>> {
        override fun onResult(response: GraphQLResponse<Message>) {
        }

        override fun onError(e: Throwable) {
        }
    }
)

However, this is too laborious.
My suggestions are:

  1. Make GsonVariablesSerializer ISO 8601 compliant.
    This is desirable because AWSDateTime is ISO 8601 compliant itself.

  2. Add an API to register type-to-type mapping
    Add an API that users can easily pass the type-to-type mapping to the serializer/deserializer.
    Or add override methods to Amplify.API.query/mutate to accept the custom Serializer.

Thanks!

[API] The request signature we calculated does not match the signature you provided

I use the master branch on the commit on Jan 15, 2020.

I used IAM authorization and Kotlin.

  1. I have amplifyconfiguration.json and I initialize Amplify with:
fun initAwsAmplify() {
    try {
        Amplify.addPlugin(AWSApiPlugin())
        Amplify.configure(applicationContext)
        Log.i("AmplifyGetStarted", "Amplify is all setup and ready to go!")
    } catch (exception: AmplifyException) {
        Log.e("AmplifyGetStarted", exception.message)
    }
}
  1. And call API with get():
fun getRequestWithIAM() {
    val options = RestOptions("mypath/next") 
    Amplify.API.get(
        "dev",
         options,
         Consumer<RestResponse> {
             Timber.d("SUCCESS: " + it.data.asJSONObject())
         },
         Consumer {
             Timber.e("REST ERROR : " + it.printStackTrace())
         }
    )
}
  1. Response

SUCCESS: {"message":"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.\n\nThe Canonical String for this request should have been\n'GET\n/dev/mypath%252Fnext\n\nhost:myhost.execute-api.eu-west-1.amazonaws.com\nx-amz-date:20200120T132539Z\nx-amz-security-token:\n\nhost;x-amz-date;x-amz-security-token*'\n\nThe String-to-Sign should have been\n'AWS4-HMAC-SHA256\n20200120T132539Z\n20200120/eu-west-1/execute-api/aws4_request*******'\n"}

So, I have a success but no real data response because I think there is a problem with the signature.
My request is https://myhost.execute-api.eu-west-1.amazonaws.com/mypath/next
With the encoding I have https://myhost.execute-api.eu-west-1.amazonaws.com/mypath%2Fnext but the Canonical String for this request should have https://myhost.execute-api.eu-west-1.amazonaws.com/mypath%252Fnext like the above message.

Have you an idea or fix for this problem?

Amplify.API.mutate

This is not really an issue on the API but more of a call for help. MutationType.CREATE is giving me an error of "Could not retrieve the response body from the returned JSON" in the onError callback. Can anyone help me debug this ?

Amplify.API.mutate(
    user,
    MutationType.CREATE,
    new ResultListener<GraphQLResponse<User>>() {
        @Override
        public void onResult(GraphQLResponse<User> response) {
            Log.i("ApiQuickStart", "Added Blog with id: " + response.getData().getId());
        }

        @Override
        public void onError(Throwable throwable) {
            // The error occurs here
            Log.e("ApiQuickStart", throwable.getLocalizedMessage());
        }
    }
);

Mutation with an S3ObjectInput list as an input

Hi I have a schema where I need an object to contian a list of photos and to store them on S3.

I noticed that the docs say:

If any mutations have an input type S3ObjectInput with fields bucket, key, region, mimeType and localUri fields, the SDK will upload the file to S3.

But since the input type is a [S3ObjectInput] I don't believe the objects are getting uploaded. Is there a workaround for this issue?

  type Mutation {
      putPostWithPhotos(
          id: ID!,
          author: String!,
          content: String
          photos: [S3ObjectInput]  #does not get uploaded to S3
      ): Post
  }
  type S3Object {
       bucket: String!
       key: String!
       region: String!
  }
  input S3ObjectInput {
       bucket: String!
       key: String!
       region: String!
       localUri: String
       mimeType: String
  }
  type Post {
      id: ID!
      author: String!
      content: String
      photos: [S3Object]
  }

Initialization in Activity#onCreate() may cause a crash or race condition

Initialization of Amplify in Activity#onCreate() may cause problems in the following scenarios though official documentation guides you to do so. Workaround is at the bottom of this issue.

  1. Start Activity and Amplify's initialization happens
Activity#onCreate()
  Amplify.addPlugin(AWSApiPlugin())
  Amplify.configure(applicationContext)
  1. Hit back button
Activity#onDestroy() called
  1. Re-launch the App again
Activity#onCreate() called again
  Amplify.addPlugin(AWSApiPlugin())
  Amplify.configure(applicationContext) // this throws AmplifyException

The reason this happens is because Amplify is a singleton object and keeps its state with static boolean configured and will not be subject to GC immediately after Activity#onDestroy() is called.
In this way, when Activity#onCreate() is called again, initialization is executed again, and a race condition occurs.
The official documentation catches the AmplifyException immediately, but because of this exception the configure per CATEGORIES is not done in Amplify#configure(), the app is obviously not in a good state.

The easiest workaround is to initialize in Application#onCreate().
This is the most realistic way. No race condition occurs because the lifetime of the Amplify object is tied to the life cycle of the Application.

// your custom application
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        try {
            Amplify.addPlugin(AWSApiPlugin())
            Amplify.configure(applicationContext)
        } catch (e: AmplifyException) {
            Log.e(TAG, e.message)
            throw RuntimeException(e)
        }
    }
}
// don't forget to add your app class in the manifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="us.shiroyama.amplifyforandroid">
    <application
        android:name=".MyApplication" />
</manifest>

Add synchronization mechanism in SQLiteStorageAdapter for SQLiteStorageHelper

The implementation now matches the naming of the method. However having multiple SQLiteStorageHelper objects for the same database will cause race conditions and crashes according to SQLite implementation.

Since SQLiteStorageAdapter has a 1:1 mapping between itself and a database, we should be having ATMOST ONE SQLiteStorageHelper for a database/SQLiteStorageAdapter object.

SQLiteStorageAdapter a = new SQLiteStorageAdapter();
SQLiteStorageAdapter b = new SQLiteStorageAdapter();

Now a.SQLiteStorageHelper != b.SQLiteStorageHelper, however a and b are talking through different SQLiteStorageHelper objects to the same physical database.

We should add sufficient synchronization mechanisms in SQLiteStorageAdapter in the future.

[DataStore] Issue with Syncing to Cloud

Hi there, we have an issue with DataStore syncing to cloud. Below is a snippet of the error:

2020-01-06 12:15:57.814 21346-21493/com.wdpr.ee.ra.android.amplify.demo W/amplify:aws-datastore: Error ended journal subscription: 
    java.lang.RuntimeException: Failed to publish item to network.
        at com.amplifyframework.datastore.network.SyncEngine$2.onResult(SyncEngine.java:219)
        at com.amplifyframework.datastore.network.SyncEngine$2.onResult(SyncEngine.java:215)
        at com.amplifyframework.datastore.network.MutationAdapter.onResult(MutationAdapter.java:39)
        at com.amplifyframework.datastore.network.MutationAdapter.onResult(MutationAdapter.java:22)
        at com.amplifyframework.api.aws.SingleItemResultOperation$OkHttpCallback.onResponse(SingleItemResultOperation.java:146)
        at okhttp3.RealCall$AsyncCall.run(RealCall.kt:138)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
        at java.lang.Thread.run(Thread.java:764)

[StorageEngine] Support storing lists and querying with predicates on them

Predicates such as beginsWith and contains currently applies only to Strings and would not parse correctly when being converted to WHERE statement in SQLite. In fact, SQLite currently does not store any collection, which makes it impossible to query on them to begin with.

Action item:

  • Implement logic to serialize lists and store them in DB
  • Implement SQL parsing logic for predicate on lists
  • Implement evaluation logic for predicate on lists

Amplify.API.mutate for Android Cannot Instantiate Blog

Describe the bug
By following the GraphQL instructions of running a Mutation, on the Blog example from the documentation, Blog cannot be instantiated.

To Reproduce
Steps to reproduce the behavior:

  1. Go to https://aws-amplify.github.io/docs/android/api
  2. Configure GraphQL
  3. Implement this method:
private void createBlog() {
    Blog blog = Blog.builder().name("My first blog").build();

    Amplify.API.mutate(
            blog,
            MutationType.CREATE,
            new ResultListener<GraphQLResponse<Blog>>() {
                @Override
                public void onResult(GraphQLResponse<Blog> response) {
                    Log.i("ApiQuickStart", "Added Blog with id: " + response.getData().getId());
                }

                @Override
                public void onError(Throwable throwable) {
                    Log.e("ApiQuickStart", throwable.getMessage());
                }
            }
    );
}

Smartphone (please complete the following information):

  • Device: [Android]
  • OS: [9]

No option to get the URL of item in S3 bucket using key.

I have files(pictures) in my S3 bucket and I am displaying them on screen. I don't want to use the Amplify.Storage.downloadFile(...)
as it takes a lot of time to download and then render. I want to use a picture loading library like glide and to do that I need to get the URL of the picture (probably by using the key).
Currently there is no such option in android.

Amplify.API.mutate error: Java.Lang.NoSuchMethodError

Describe the bug
So I am following the tutorial at the website and I get this weird error

2019-12-20 04:30:12.284 9922-9982/com.sum.idgaf D/MainActivity: API QuickStart ready
2019-12-20 04:30:12.325 9922-9982/com.sum.idgaf E/AndroidRuntime: FATAL EXCEPTION: Thread-4
Process: com.sum.idgaf, PID: 9922
java.lang.NoSuchMethodError: No static method metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; in class Ljava/lang/invoke/LambdaMetafactory; or its super classes (declaration of 'java.lang.invoke.LambdaMetafactory' appears in /apex/com.android.runtime/javalib/core-oj.jar)
at com.amplifyframework.util.FieldFinder.findFieldsIn(FieldFinder.java:59)
at com.amplifyframework.core.model.ModelSchema.fromModelClass(ModelSchema.java:105)
at com.amplifyframework.api.aws.AppSyncGraphQLRequestFactory.buildMutation(AppSyncGraphQLRequestFactory.java:155)
at com.amplifyframework.api.aws.AWSApiPlugin.mutate(AWSApiPlugin.java:341)
at com.amplifyframework.api.aws.AWSApiPlugin.mutate(AWSApiPlugin.java:322)
at com.amplifyframework.api.aws.AWSApiPlugin.mutate(AWSApiPlugin.java:277)
at com.amplifyframework.api.ApiCategory.mutate(ApiCategory.java:135)
at com.sum.idgaf.MainActivity.createBlog(MainActivity.java:51)
at com.sum.idgaf.MainActivity.access$100(MainActivity.java:19)
at com.sum.idgaf.MainActivity$1.onResult(MainActivity.java:35)
at com.sum.idgaf.MainActivity$1.onResult(MainActivity.java:28)
at com.amazonaws.mobile.client.internal.InternalCallback.call(InternalCallback.java:75)
at com.amazonaws.mobile.client.internal.InternalCallback.onResult(InternalCallback.java:62)
at com.amazonaws.mobile.client.AWSMobileClient$2.run(AWSMobileClient.java:452)
at com.amazonaws.mobile.client.internal.InternalCallback$1.run(InternalCallback.java:101)
at java.lang.Thread.run(Thread.java:919)

To Reproduce
https://aws-amplify.github.io/docs/android/api

upto createBlog()

    private void createBlog() {
        Blog blog = Blog.builder().name("My first blog").build();

        Amplify.API.mutate(
                blog,
                MutationType.CREATE,
                new ResultListener<GraphQLResponse<Blog>>() {
                    @Override
                    public void onResult(GraphQLResponse<Blog> response) {
                        Log.i("ApiQuickStart", "Added Blog with id: " + response.getData().getId());
                    }

                    @Override
                    public void onError(Throwable throwable) {
                        Log.e("ApiQuickStart", throwable.getMessage());
                    }
                }
        );
    }

Which AWS service(s) are affected?
Amplify API

Expected behavior
It is supposed to create an entry in the database

Screenshots
If applicable, add screenshots to help explain your problem.

Environment Information (please complete the following information):

  • AWS Android SDK Version: [2.7.+]
  • Device: [Nexus 6]
  • Android Version: [API 29]
  • Specific to simulators: [No]

Additional context

[DataStore] Freeze on startup

Hi there, we have issue with the datastore module. Below is a snippet of the demo app:

public final class AmplifyDemoApplication extends android.app.Application {
    private static final String TAG = "AmplifyDemoApplication";

    @Override
    public void onCreate() {
        super.onCreate();
        AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
        try {
            AWSDataStorePlugin dataStorePlugin = AWSDataStorePlugin.forModels(AmplifyModelProvider.getInstance());
            Amplify.addPlugin(dataStorePlugin);
            Amplify.configure(getApplicationContext());
        } catch (AmplifyException exception) {
            Log.e("AmplifyGetStarted", exception.getMessage());
        }
    }
}

Here's my basic schema.graphql:

type Event @model {
    id: ID!
    name: String!
    location: String
    startTime: String
    description: String
}

When debugging, It looks like it's stuck in an infinite loop, while trying to Amplify.configure

Problems with datastore

I have been trying to initialize app sync according to the Amazon guide and I get the message that the connection could be made using the following code

AWSMobileClient.getInstance()
            .initialize(applicationContext, object : Callback<UserStateDetails> {
                override fun onResult(userStateDetails: UserStateDetails) {
                    try {

                        Amplify.addPlugin(AWSApiPlugin()) // If using remote model synchronization
                        Amplify.configure(applicationContext)
                        println("ApiQuickStart " + "All set and ready to go!")
                    } catch (e: Exception) {
                        println("ApiQuickStart ERROR " + e.message)
                        println("ERRROR ApiQuickStart")
                    }

                }

                override fun onError(e: Exception) {
                    println("ApiQuickStart Initialization error. " + e)
                }
            })

ApiQuickStart All set and ready to go!

But when you try to initialize the service with datastore I get the following error message

Code

AWSMobileClient.getInstance()
            .initialize(applicationContext, object : Callback<UserStateDetails> {
                override fun onResult(userStateDetails: UserStateDetails) {
                    try {
                        val dataStorePlugin = AWSDataStorePlugin.forModels(AmplifyModelProvider.getInstance())
                        Amplify.addPlugin(dataStorePlugin)
                        Amplify.addPlugin(AWSApiPlugin()) // If using remote model synchronization
                        Amplify.configure(applicationContext)
                        println("ApiQuickStart " + "All set and ready to go!")
                    } catch (e: Exception) {
                        println("ApiQuickStart ERROR " + e.message)
                        println("ERRROR ApiQuickStart")
                    }

                }

                override fun onError(e: Exception) {
                    println("ApiQuickStart Initialization error. " + e)
                }
            })

Error

ApiQuickStart ERROR com.amplifyframework.datastore.DataStoreException: Error in initializing the SQLiteStorageAdapter

My gradles

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"
    defaultConfig {
        applicationId "com.android.example"
        minSdkVersion 15
        targetSdkVersion 29
        vectorDrawables.useSupportLibrary = true
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        multiDexEnabled = true
        android.defaultConfig.vectorDrawables.useSupportLibrary = true
    }
    dataBinding {
        enabled = true
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'com.google.android.material:material:1.1.0'
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    implementation 'com.amplifyframework:core:0.9.1'
    implementation 'com.amplifyframework:aws-datastore:0.9.1'
    implementation 'com.amplifyframework:aws-api:0.9.1'
    implementation 'androidx.multidex:multidex:2.0.1'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    implementation "android.arch.navigation:navigation-fragment-ktx:$version_navigation"
    implementation "android.arch.navigation:navigation-ui-ktx:$version_navigation"
    implementation 'androidx.gridlayout:gridlayout:1.0.0'
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

}
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {

    ext{
        ext.kotlin_version = '1.3.70'
        version_navigation = '1.0.0'
    }
    repositories {
        google()
        jcenter()
        mavenCentral()

    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.android.tools.build:gradle:3.6.1'
        //classpath 'com.amplifyframework:amplify-tools-gradle-plugin-beta:0.1.0'
        classpath 'com.amplifyframework:amplify-tools-gradle-plugin:0.2.1'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

//apply plugin: 'com.amplifyframework.amplifytools'

allprojects {
    repositories {
        google()
        jcenter()
        
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

Amplify.API.query, Amplify.API.mutate and Amplify.API.subscribe are blocking

Amplify.API.query, Amplify.API.mutate and Amplify.API.subscribe seem to be blocking APIs.
Is this intentional? I expected these APIs to be asynchronous APIs because it is an API that passes the ResultListener callback.
Anyway, I didn't want to block the UI in my app, so I wrapped it in Rx as follows:

// query
val source: SingleOnSubscribe<List<Message>> = SingleOnSubscribe {
    val listener = object :
        ResultListener<GraphQLResponse<Iterable<Message>>> {
        override fun onResult(response: GraphQLResponse<Iterable<Message>>) {
            val messages: List<Message> = response?.data?.map { it }.orEmpty()
            it.onSuccess(messages)
        }

        override fun onError(e: Throwable) {
        }
    }
    Amplify.API.query(request, listener)
}
return Single.create(source)

// mutation
return Completable.create { emitter ->
    Amplify.API.mutate(
        request,
        object :
            ResultListener<GraphQLResponse<Message>> {
            override fun onResult(response: GraphQLResponse<Message>) {
                response.data?.let {
                    emitter.onComplete()
                }
            }

            override fun onError(e: Throwable) {
                emitter.onError(e)
            }
        })
}

// subscription
return Single.create { emitter ->
    Amplify.API.subscribe(
        Message::class.java,
        SubscriptionType.ON_CREATE,
        object :
            StreamListener<GraphQLResponse<Message>> {
            override fun onNext(response: GraphQLResponse<Message>) {
                response?.data?.let {
                    emitter.onSuccess(it)
                }
            }

            override fun onComplete() {
            }

            override fun onError(e: Throwable) {
                emitter.onError(e)
            }
        }
    )
}

Caller should use this as follows:

compositeDisposable.add(
    mutation(
        name,
        editTextContent.text.toString(),
        DEFAULT_ROOM
    )
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribeBy(
            onComplete = {
                editTextContent.setText("")
            },
            onError = {
                Log.e(TAG, it.message, it)
            }
        )
)

I hope it helps those with similar problems.

CI/CD Infrastructure

  1. Currently we rely on the developer running ./gradlew build locally before pushing any code. We want to introduce a CircleCI process for this.

  2. We are using the following spec:

Android Platform 29
Java 1.8 Source compatibility

We need to install these tools as part of the build script.

[API] Gson serializer does not handle timezone offset in seconds

AWSDate supports seconds field in the timezone offset, which is not supported by ISO8601 format. This causes Gson to fail to parse date such as "1970-01-01-01:00:00".

Sample code to reproduce:

Gson gson = new Gson();
gson.fromJson("\"1970-01-01-01:00:00\"", Date.class);

logcat report from integration test (breaks CircleCI workflow):

    --------- beginning of crash
2020-01-06 16:59:13.548 31371-31406/com.amplifyframework.api.aws.test E/AndroidRuntime: FATAL EXCEPTION: OkHttp Dispatcher
    Process: com.amplifyframework.api.aws.test, PID: 31371
    com.google.gson.JsonSyntaxException: 1970-01-01-07:00:39
        at com.google.gson.internal.bind.DateTypeAdapter.deserializeToDate(DateTypeAdapter.java:87)
        at com.google.gson.internal.bind.DateTypeAdapter.read(DateTypeAdapter.java:75)
        at com.google.gson.internal.bind.DateTypeAdapter.read(DateTypeAdapter.java:46)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:131)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:222)
        at com.google.gson.Gson.fromJson(Gson.java:932)
        at com.google.gson.Gson.fromJson(Gson.java:1003)
        at com.google.gson.Gson.fromJson(Gson.java:975)
        at com.amplifyframework.api.aws.GsonGraphQLResponseFactory.parseDataAsList(GsonGraphQLResponseFactory.java:198)
        at com.amplifyframework.api.aws.GsonGraphQLResponseFactory.buildSingleArrayResponse(GsonGraphQLResponseFactory.java:132)
        at com.amplifyframework.api.graphql.GraphQLOperation.wrapMultiResultResponse(GraphQLOperation.java:71)
        at com.amplifyframework.api.aws.SingleArrayResultOperation.access$100(SingleArrayResultOperation.java:47)
        at com.amplifyframework.api.aws.SingleArrayResultOperation$OkHttpCallback.onResponse(SingleArrayResultOperation.java:140)
        at okhttp3.RealCall$AsyncCall.run(RealCall.kt:138)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:764)
     Caused by: java.text.ParseException: Failed to parse date ["1970-01-01-07:00:39"]: Mismatching time zone indicator: GMT-07:00:39 given, resolves to GMT
        at com.google.gson.internal.bind.util.ISO8601Utils.parse(ISO8601Utils.java:274)
        at com.google.gson.internal.bind.DateTypeAdapter.deserializeToDate(DateTypeAdapter.java:85)
        at com.google.gson.internal.bind.DateTypeAdapter.read(DateTypeAdapter.java:75) 
        at com.google.gson.internal.bind.DateTypeAdapter.read(DateTypeAdapter.java:46) 
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:131) 
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:222) 
        at com.google.gson.Gson.fromJson(Gson.java:932) 
        at com.google.gson.Gson.fromJson(Gson.java:1003) 
        at com.google.gson.Gson.fromJson(Gson.java:975) 
        at com.amplifyframework.api.aws.GsonGraphQLResponseFactory.parseDataAsList(GsonGraphQLResponseFactory.java:198) 
        at com.amplifyframework.api.aws.GsonGraphQLResponseFactory.buildSingleArrayResponse(GsonGraphQLResponseFactory.java:132) 
        at com.amplifyframework.api.graphql.GraphQLOperation.wrapMultiResultResponse(GraphQLOperation.java:71) 
        at com.amplifyframework.api.aws.SingleArrayResultOperation.access$100(SingleArrayResultOperation.java:47) 
        at com.amplifyframework.api.aws.SingleArrayResultOperation$OkHttpCallback.onResponse(SingleArrayResultOperation.java:140) 
        at okhttp3.RealCall$AsyncCall.run(RealCall.kt:138) 
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) 
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) 
        at java.lang.Thread.run(Thread.java:764) 
     Caused by: java.lang.IndexOutOfBoundsException: Mismatching time zone indicator: GMT-07:00:39 given, resolves to GMT
        at com.google.gson.internal.bind.util.ISO8601Utils.parse(ISO8601Utils.java:240)
        at com.google.gson.internal.bind.DateTypeAdapter.deserializeToDate(DateTypeAdapter.java:85) 
        at com.google.gson.internal.bind.DateTypeAdapter.read(DateTypeAdapter.java:75) 
        at com.google.gson.internal.bind.DateTypeAdapter.read(DateTypeAdapter.java:46) 
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:131) 
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:222) 
        at com.google.gson.Gson.fromJson(Gson.java:932) 
        at com.google.gson.Gson.fromJson(Gson.java:1003) 
        at com.google.gson.Gson.fromJson(Gson.java:975) 
        at com.amplifyframework.api.aws.GsonGraphQLResponseFactory.parseDataAsList(GsonGraphQLResponseFactory.java:198) 
        at com.amplifyframework.api.aws.GsonGraphQLResponseFactory.buildSingleArrayResponse(GsonGraphQLResponseFactory.java:132) 
        at com.amplifyframework.api.graphql.GraphQLOperation.wrapMultiResultResponse(GraphQLOperation.java:71) 
        at com.amplifyframework.api.aws.SingleArrayResultOperation.access$100(SingleArrayResultOperation.java:47) 
        at com.amplifyframework.api.aws.SingleArrayResultOperation$OkHttpCallback.onResponse(SingleArrayResultOperation.java:140) 
        at okhttp3.RealCall$AsyncCall.run(RealCall.kt:138) 
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) 
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) 
        at java.lang.Thread.run(Thread.java:764) 

[API] GraphQL - Support for different Id fields and range keys

Amplify Android imposes an unreasonable requirement that every model must have an ID field called exactly id. I see no reason why this particular name is hardcoded everywhere, and it is a significant limitation when dealing with schema created before. Also there is no support for range (sort) keys.

From the source:

doc.append("query ")
    .append("Get")
    .append(StringUtils.capitalizeFirst(graphQlTypeName))
    .append("(")
    .append("$id: ID!) { get")
    .append(StringUtils.capitalizeFirst(graphQlTypeName))
    .append("(id: $id) { ")
    .append(getModelFields(modelClass, DEFAULT_LEVEL_DEPTH))
    .append("}}");

variables.put("id", objectId);

[API] Subscribe fails if model has owner authorization

If a model has @auth(rules: [{allow: owner}]) AppSync requires an owner parameter be passed to the subscribe function but we currently are not generating this when we build the GraphQL Request for subscribe on these models.

Furthermore, the CLI model gen does not indicate which models have this type of auth so we would not be able to know when to generate it.

Sync Engine will try to publish a change twice (TODO: why?)

     Caused by: java.lang.RuntimeException: Failed to publish item to network.
        at com.amplifyframework.datastore.syncengine.MutationProcessor.lambda$null$3(SourceFile:5) 
        at com.amplifyframework.datastore.syncengine.MutationProcessor$$Lambda$8.accept(Unknown Source:6) 
        at com.amplifyframework.datastore.appsync.AppSyncClient$$Lambda$5.accept(SourceFile:2) 
        at com.amplifyframework.api.aws.SingleItemResultOperation$OkHttpCallback.onResponse(SourceFile:20) 
        at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:504) 
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) 
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) 
        at java.lang.Thread.run(Thread.java:919) 

Add documentation for Kotlin

Which AWS Services is the feature request for?
All

Describe the solution you'd like

  • Currently, in the AWS Amplify Android docs everything is written in Java. However, I am looking to use Kotlin for my next project.
  • I found https://aws-amplify.github.io/docs/android/api?platform=kotlin which shows the tag for Kotlin on the top right. But all the code snippets are actually in Java.
  • It would be really helpful if those code snippets could be changed to Kotlin.

Lambdafication of Android Category Behaviors

The current syntax required when using our category behaviors is pretty verbose. For example, a user passes an anonymous instance of ResultListener or StreamListener when invoking an API behavior:

Amplify.API.query(Person.class, new ResultListener<Iterable<Person>> {
    @Override
    public void onResult(Iterable<Person> people) {
        Log.i(TAG, "Found people via remote API: " + people);
    }
    
    @Override
    public void onError(Throwable error) {
        Log.e(TAG, "Error querying the API.", error);
    }
});

The ResultListener above is one of two widely-used callback interfaces defined in our Core library. The ResultListener and its sibling StreamListener are used extensively throughout the category contracts. These two callback interfaces had been modeled after Rx’s Observer and SingleObserver:

// A poor man's io.reactivex.SingleObserver
interface ResultListener<T> {
    void onResult(T result);
    void onError(Throwable error);
}
// A poor man's io.reactivex.Observer
interface StreamListener<T> {
    void onNext(T item);
    void onComplete();
    void onError(Throwable error);
}

However, consumers of Rx rarely use these, directly. Rx provides a family of convenience methods to subscribe to an Observable or to a Single, by providing lambdas. For example, instead of specifying an Observer instance during the call to subscribe(..), an Rx user instead does:

someObservable.subscribe(
    /* onNext */ item -> Log.i(TAG, "Got an item: " + item),
    /* onError */ error -> Log.e(TAG, "An error occurred.", error)
);

That’s 4 lines of code, instead of 11 like we have with the ResultListener in our API behavior.

The core problem is that ResultListener cannot be expressed as a lambda, since it is not a simple functional interface as defined in the Java Language Specification (JLS).

A Proposal

Rx addresses this problem by using a Consumer, which accepts an argument in its callback, and an Action, which does not.

A solution is to remove ResultListener and StreamListener from the public contracts. Instead, we replace them with decomposed functional interfaces, which are able to be expressed as lambdas:

@FunctionalInterface interface Consumer<T> {
    void accept(T item);
}
@FunctionalInterface interface Action {
    void call();
}

With these, a StreamListener would become a three-tuple of:

  1. Consumer<T> (for an onNext(T item) callback);
  2. Consumer<E> (with E extends AmplifyException, for an onError(E error) callback);
  3. Action (for an onComplete() callback.)

And the ResultListener would become a two-tuple of:

  1. Consumer<T> (to replace the onResult(T item) callback);
  2. Consumer<E> (with E extends AmplifyException, to supply an onError(E error) callback.)

As a worked example, the API query(...) method would become:

void <T extends Model> query(
        Class<T> clazz,
        Consumer<Iterable<T>> onItems,
        Consumer<ApiException> onError);

And a customer would invoke it as:

Amplify.API.query(Person.class,
    items -> Log.i(TAG, "Items: " + items), 
    error -> Log.w(TAG, "Error during query.", error)
);

Internally, we may keep the ResultListener and StreamListener as “function bundles,” to group together the individually specified callbacks:

// A bundle for customer-provided callbacks.
// This keeps a the callbacks for a single API call "together",
// in a single piece of state.
final class ResultListener<T, E extends AmplifyException> {
    private final Consumer<T> onResult;
    private final Consumer<E> onError;
    
    ResultListener(Consumer<T> onResult, Consumer<E> onError) {
        this.onResult = onResult;
        this.onError = onError;
    }

    void onResult(T result) {
        onResult.accept(result);
    }

    void onError(E error) {
        onError.accept(error);
    }
}   

CircleCI improvements

Possible enhancements :

  1. Parallelize tests for different modules
  2. Consolidate all the configuration files into one config file
  3. Upload test artifacts for analysis and debugging.

[Predicates] Predicate design

Dev would write:

import static com.amplifyframework.core.model.query.predicate.QueryField;

 API.query(Person.class, 
                  field(“id”).eq(“1234”), 
                  QueryType.GET, 
                  callback)

 dataStore.query(Person.class, 
                  field(“id”).eq(“1234”),
                  callback)

This would generate:

public class QueryPredicateOperation implements QueryPredicate {
   String field = "id";

   EqualQueryOperator (extends QueryOperator) operator = {
          QueryOperatorType (enum) = EQUAL
          Object value = "1234"
   }
}

Amplify.Api.Get fails when attempting to pass multiple PathParameters

Amplify.Api.Get fails when attempting to pass multiple PathParameters in RestOptions.
The slash ("/") gets converted to %2F. This seems to be a expetected behavious from okhttp3.

sdk version: 2.16.9

To Reproduce

final Map parameters = new HashMap<>();
parameters.put("someParam", "someValue");
RestOptions options = new RestOptions("path1/path2", parameters);
Amplify.API.get("backend", options, new ResultListener<RestResponse>() {
     @Override
     public void onResult(RestResponse restResponse) {
        Log.i("SUCCESS", "" + restResponse.toString());
     }
     @Override
     public void onError(Throwable throwable) {
         Log.e("RESTERROR", throwable.toString());
     }
});

the RestOperationRequestUtils.constructURL uses HttpUrl.Builder().addPathSegment(urlPath), this operation replaces the "/" for "%2F".

The code above would create something like:
www.example.com/v1/path1%2Fpath2?someParam=someValue

Expected behavior
Don't convert / to %2f in the url when passing a path containing slashes or enahnce the RestOptions.builder with the ability to add multiple paths.

eg:

RestOptions.builder()
            .addPath("path1")
            .addPath("path2")
            .build();

onResult callbacks has same JVM signature

Screen Shot 2020-02-18 at 11 40 39 PM

This may not be a very big issue. Got the JVM signature error when tried to implements these two interfaces Callback,ResultListener<GraphQLResponse>

It shouldn't be a show stopper when we needs to implement multiple APIs or more interface on single activity/class.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.