Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Kotlin Extensions for Amplify Libraries #605

Closed
jpignata opened this issue Jun 26, 2020 · 5 comments · Fixed by #1057
Closed

RFC: Kotlin Extensions for Amplify Libraries #605

jpignata opened this issue Jun 26, 2020 · 5 comments · Fixed by #1057
Assignees
Labels
rfc Request for comments

Comments

@jpignata
Copy link
Contributor

jpignata commented Jun 26, 2020

The Amplify Libraries for Android are written in Java. Increasingly, we're hearing from developers that you are using Kotlin. In recognition of this, we would like to provide a more idiomatic interface for using Amplify from Kotlin. We're considering building a Kotlin Extensions library. The extensions would provide a Kotlin-first experience to developers integrating the Amplify Libraries in their Kotlin-based Android applications.

We're requesting your feedback on this general product direction, and have some specific questions:

  • Are you building applications in Java or Kotlin?
  • If you're using the Amplify Libraries in a Kotlin application, what are the points of friction you've encountered?
  • How would you expect to interact with a Kotlin facade for the Amplify Libraries? Are there any patterns you'd prefer to see implemented?

Please comment on this thread with your thoughts, suggestions, nits, complaints, or anything else on your mind.

Reactions (👍 , 👎 , etc.) are also welcomed, as they help us weigh the relative importance of this work.

Thank you!

@jpignata jpignata added the rfc Request for comments label Jun 26, 2020
@jpignata jpignata pinned this issue Jun 26, 2020
@jamesonwilliams jamesonwilliams unpinned this issue Sep 20, 2020
@jamesonwilliams jamesonwilliams pinned this issue Sep 20, 2020
@shichonghuotian
Copy link

shichonghuotian commented Oct 13, 2020

I hope amplify can support the coroutine.Now we can use suspendCancellableCoroutine add support for coroutines,but "Amplify Auth" does not use the thread pool and cannot cancel the request

@Blu3Parr0t
Copy link

I am building applications in Kotlin. I'm also an Android Contractor. A lot of jobs I get are requiring Kotlin experience due to most companies wanting to migrate to Kotlin. There is still Java needed for maintaining old frameworks that aren't being migrated over, but most new features are being created with Kotlin in mind.

The main friction I'm having right now is making a wrapper utility function that would be able to handle all of Amplify calls from sign up, login, queries, and mutations in a more generic way. Since I'm used to using Retrofit I've been able to pass in suspend functions and wrap them with the ServiceResult sealed class (https://developer.android.com/jetpack/guide). This has helped me to be able to handle all types of calls in a more generic manner in order to catch errors.

Facade could work. As mentioned above the way I'd develop to handle the Amplify calls would be to implement a singleton utility function that would expect a suspend fun and return a Response where that may be for sing up, log in, mutation, queries. There might be more to unpack on how to effectively deliver the payload to a client, but one of the biggest issues we have at least in Android is being able to cover error cases. Most companies want us to implement some sort of mechanism that will catch Errors. That requires us to keep in mind not only when the API is down, but if the API returns null, user is has wireless off, etc. Most companies I've been to have had devs create base model classes that have the error fields and are extended to their unique model of expected data in order to handle false positives from the back end. Outside of that they implement something like this:

object GraphQLHandler {

    suspend fun <T> handleCall(
        call: suspend () -> Response<T>
    ): ServiceResult<T> {
         return try {
             val serviceCall = call()
             val body = serviceCall.awsResponse
             if(body != null){
                 ServiceResult.Success(body)
             } else {
                 ServiceResult.Error(Exception("Null body"))
             }
         } catch (exception: Exception){
             ServiceResult.Error(exception)
         }
    }
}

In their repo layer they might call something like a login, sign up, query, mutation:

suspend fun logIn(username: String, password: String): ServiceResult<LogInModel> {
        return withContext(dispatchers.IO) {
            GraphQLHandler.handleCall {
                Amplify.Auth.LogIn(
                    username,
                    password
                )
            }

Lastly, I'd like to say that most Android Devs use something like Retrofit. Almost all Android Devs are trained or expected to know Retrofit when they join a job. I would keep that in mind when wanting to deliver to the Android system because it will automatically let a lot of developers know exactly how to go about switching a Retrofit component with an Amplify component

@jamesonwilliams
Copy link
Contributor

jamesonwilliams commented Dec 14, 2020

Thanks @Blu3Parr0t & @shichonghuotian for the feedback! In your replies, I see two unrelated feature requests:

  1. Make auth calls cancellable, and
  2. Improve integration with Retrofit.

As far as Kotlin support itself is concerned, here are my thoughts. We will produce another "facade" module, like rxbindings, but for Kotlin. I suggest to call it either ktxbindings or just ktx. The primary goal of this module will be contain wrapper functions. The functions will convert Amplify's vanilla callback-style method signatures into Kotlin suspending functions.

For example, DataStore's vanilla save(...) API can be converted to a suspending function:

suspend fun <T: Model> save(model: T): Unit =
    suspendCoroutine { cont ->
        Amplify.DataStore.save(model,
            { cont.resume(Unit) },
            { cont.resumeWithException(it) }
        )
    }

As we saw in the Rx Bindings, the Amplify methods fall generally into three categories:

  1. Methods that render a single result (or a void result), and are not cancelable
  2. Methods that render a single result (or a void result), but can be cancelled
  3. Methods that emit streams of values and/or multiple different value types

The save(...) example above is of type (1).

API's mutate is an example of the second type. It needs a mechanism for cancellation. To achieve this, we can use suspendCancellableCoroutine to bridge the gap:

suspend fun <T> mutate(request: GraphQLRequest<T>): GraphQLResponse<T> =
    suspendCancellableCoroutine { cont ->
        val operation = Amplify.API.mutate(request,
            { cont.resume(it) },
            { cont.resumeWithException(it) }
        )
        cont.invokeOnCancellation { operation?.cancel() }
    }

When launched in a coroutine scope, this will produce a Job that can be cancel()ed:

val job = launch {
    val response = mutate(request)
}
job.cancel()

Lastly, API's subscribe method, and Storage's progress-reporting upload and download have multiple callback types. Ultimately these multiple types will need to be enveloped into a single result structure, as happens in the Rx Bindings. Within that result structure, there must be a way to model the stream of subscription events / progress updates. I suggest to bundle them into a Kotlin Flow by using callbackFlow.

Another important thing to figure out: do we namespace these methods in a new facade, like Kamplify.API.mutate(...)? Or can we use Kotlin extensions themselves to achieve a call on the Amplify facade itself, like:

val response = Amplify.API.mutate(request)

cc: @brady-aiello @aajtodd

@aajtodd
Copy link

aajtodd commented Dec 14, 2020

@jamesonwilliams

For (1), (2), and (3) that looks like what I would expect I think. Flows are a like a reactive stream from my understanding so makes sense. I would probably want to draw up an actual RFC proposal/design of course to be sure but overall agreed.

As far as naming, I'd probably name it something like amplify-android-coroutines or coroutine-bindings.

ktx is not very descriptive and coroutines in the name follows the integration pattern Kotlin established for going the other way and binding xyz to coroutines:
https://github.com/Kotlin/kotlinx.coroutines/tree/master/integration
https://github.com/Kotlin/kotlinx.coroutines/tree/master/reactive

As far as new facade vs extensions, I would go with extensions barring any complications with that route. This is what those kotlinx libraries do as well for marrying coroutines to other frameworks and async primitives.

jamesonwilliams added a commit that referenced this issue Jan 13, 2021
This commit introduces a new, optional core-ktx module. The module
includes improved support for using Amplify from Kotlin.

```gradle
dependencies {
    implementation "com.amplifyframework:core-ktx:$version"
}
```

core-ktx is largely comprised of extension functions, which provide new
means of interacting with the various Amplify categories (API, Auth,
DataStore, Predictions, Storage.) These functions include several
improvements to the Kotlin developer experience, mainly by adding
support for Coroutines.

Amplify has several types of APIs. Some are synchronous calls, which
immediately return a value. However, most Amplify behaviors are
asynchronous calls. Among the async calls, there are a few broad
categories:

 1. Functions that return a single value, and cannot be canceled;
 2. Functions that return a single value, and can be canceled;
 3. Functions that emit a stream of values
 4. Functions that emit multiple types of values

Most Amplify behaviors are of type (1). Auth and Predictions are
entirely comprised of type (1). The Kotlin flavors of these behaviors
are expressed as suspending functions, e.g.:

```kotlin
suspend fun AuthCategory.signOut() {
   ...
}
```

The simplest developer experience to invoke this method will be:
```kotlin
runBlocking {
    Amplify.Auth.signOut()
}
```

Some single-valued functions, such as the `mutate(...)` behavior in the
API category, may be canceled before rendering a result. The Kotlin
version of `mutate(...)` is expressed this way:

```kotlin
suspend fun <R> ApiCategory.mutate(
        graphQlRequest: GraphQLRequest<R>): GraphQLResponse<R> {
    ...
}
```

The user may cancel the behavior via Kotlin's `Job` construct:
```kotlin
val job = launch(Dispatchers.IO) {
    Amplify.API.mutate(request)
}
...
job.cancel()
```

There are also some Amplify beahviors which a stream of values.
DataStore's `observe()` is a canonical example. It's extension function
is expressed as:

```kotlin
fun DataStoreCategory.observe(): Flow<DataStoreItemChange<out Model>> {
    ...
}
```

A developer may interact with the flow in this way:
```kotlin
Amplify.DataStore.observe()
    .collect { print(it) }
```

API's `subscribe()` also emits a stream of values. However, it is also
important to know about the _lifecycle_ of a GraphQL subscription. So,
this method returns an operation structure, which envelopes two flows:
```kotlin
fun <T> ApiCategory.subscribe(
        graphQlRequest: GraphQLRequest<T>): GraphQLSubscriptionOperation<T> {
    ...
}
```

A developer can inspect the connection state, as well as the stream of
subscription data:
```kotlin
val subscription = Amplify.API.subscribe(request)
subscription.events.collect {
    print("Got a subscription event: $it")
}
subscription.connectionState.collect {
    print("Connection state changed: $it")
}
```

The Storage category's various upload and download functions also
exhibit a similar pattern. We want obtain the result, but we may also
like to observe a stream of progress updates. The signature looks like:
```kotlin
fun StorageCategory.downloadFile(
        key: String,
        local: File,
): InProgressStorageOperation<StorageDownloadFileResult> {
```

A developer can observe the download progress via a Flow:
```kotlin
val download = Amplify.Storage.downloadFile("s3Key", local)
download.progress.collect { print("Progress: $it") }
```
Or, the developer can access the result of the download via a suspend
function exposed on the download operation:
```kotlin
val result = runBlocking { download.result }
```

Refer: #605
@jamesonwilliams jamesonwilliams unpinned this issue Jan 25, 2021
jamesonwilliams added a commit that referenced this issue Feb 12, 2021
This commit introduces a new, optional core-ktx module. The module
includes improved support for using Amplify from Kotlin.

To use the Kotlin facade, include this dependency:
```gradle
dependencies {
    implementation "com.amplifyframework:core-ktx:$version"
}
```
And import the Kotlin facade instead of the one in `core`:
```kotlin
import com.amplifyframework.kotlin.Amplify
```

core-ktx introduces an alternate `Amplify` facade, which provides new
means of interacting with the various Amplify categories (API, Auth,
DataStore, Predictions, Storage.) The new facade include several
improvements to the Kotlin developer experience, mainly by adding
support for Coroutines.

Amplify has several types of APIs. Some are synchronous calls, which
immediately return a value. However, most Amplify behaviors are
asynchronous calls. Among the async calls, there are a few broad
categories:

 1. Functions that return a single value, and cannot be canceled;
 2. Functions that return a single value, and can be canceled;
 3. Functions that emit a stream of values
 4. Functions that emit multiple types of values

Most Amplify behaviors are of type (1). Auth and Predictions are
entirely comprised of type (1). The Kotlin flavors of these behaviors
are expressed as suspending functions, e.g.:

```kotlin
suspend fun signOut() {
   ...
}
```

The simplest developer experience to invoke this method will be:
```kotlin
runBlocking {
    Amplify.Auth.signOut()
}
```

Some single-valued functions, such as the `mutate(...)` behavior in the
API category, may be canceled before rendering a result. The Kotlin
version of `mutate(...)` is expressed this way:

```kotlin
suspend fun <R> ApiCategory.mutate(
        request: GraphQLRequest<R>, apiName: String? = null)
        : GraphQLResponse<R> {
    ...
}
```

The user may cancel the behavior via Kotlin's `Job` construct:
```kotlin
val job = launch(Dispatchers.IO) {
    Amplify.API.mutate(request)
}
...
job.cancel()
```

There are also some Amplify behaviors which emit a stream of values.
DataStore's `observe()` is a canonical example. It's extension function
is expressed as:

```kotlin
fun observe(): Flow<DataStoreItemChange<out Model>> {
    ...
}
```

A developer may interact with the flow in this way:
```kotlin
Amplify.DataStore.observe()
    .collect { print(it) }
```

API's `subscribe()` also emits a stream of values. However, it is also
important to know about the _lifecycle_ of a GraphQL subscription. So,
this method returns an operation structure, which envelopes two flows:
```kotlin
fun <T> ApiCategory.subscribe(
        request: GraphQLRequest<T>, apiName: String? = null)
        : GraphQLSubscriptionOperation<T> {
    ...
}
```

A developer can inspect the connection state, as well as the stream of
subscription data:
```kotlin
val subscription = Amplify.API.subscribe(request)
subscription.subscriptionData.collect {
    print("Got a subscription data: $it")
}
subscription.connectionState.collect {
    print("Connection state changed: $it")
}
```

The Storage category's various upload and download functions also
exhibit a similar pattern. We want obtain the result, but we may also
like to observe a stream of progress updates. The signature looks like:
```kotlin
fun StorageCategory.downloadFile(
        key: String,
        local: File,
): InProgressStorageOperation<StorageDownloadFileResult> {
```

A developer can access the result of the download via a suspend
function exposed on the download operation:
```kotlin
val result = runBlocking { download.result() }
```

Refer: #605
@jamesonwilliams jamesonwilliams linked a pull request Feb 18, 2021 that will close this issue
jamesonwilliams added a commit that referenced this issue Feb 19, 2021
This commit introduces a new, optional core-ktx module. The module
includes improved support for using Amplify from Kotlin.

To use the Kotlin facade, include this dependency:
```gradle
dependencies {
    implementation "com.amplifyframework:core-ktx:$version"
}
```
And import the Kotlin facade instead of the one in `core`:
```kotlin
import com.amplifyframework.kotlin.Amplify
```

core-ktx introduces an alternate `Amplify` facade, which provides new
means of interacting with the various Amplify categories (API, Auth,
DataStore, Predictions, Storage.) The new facade include several
improvements to the Kotlin developer experience, mainly by adding
support for Coroutines.

Amplify has several types of APIs. Some are synchronous calls, which
immediately return a value. However, most Amplify behaviors are
asynchronous calls. Among the async calls, there are a few broad
categories:

 1. Functions that return a single value, and cannot be canceled;
 2. Functions that return a single value, and can be canceled;
 3. Functions that emit a stream of values
 4. Functions that emit multiple types of values

Most Amplify behaviors are of type (1). Auth and Predictions are
entirely comprised of type (1). The Kotlin flavors of these behaviors
are expressed as suspending functions, e.g.:

```kotlin
suspend fun signOut() {
   ...
}
```

The simplest developer experience to invoke this method will be:
```kotlin
runBlocking {
    Amplify.Auth.signOut()
}
```

Some single-valued functions, such as the `mutate(...)` behavior in the
API category, may be canceled before rendering a result. The Kotlin
version of `mutate(...)` is expressed this way:

```kotlin
suspend fun <R> ApiCategory.mutate(
        request: GraphQLRequest<R>, apiName: String? = null)
        : GraphQLResponse<R> {
    ...
}
```

The user may cancel the behavior via Kotlin's `Job` construct:
```kotlin
val job = launch(Dispatchers.IO) {
    Amplify.API.mutate(request)
}
...
job.cancel()
```

There are also some Amplify behaviors which emit a stream of values.
DataStore's `observe()` is a canonical example. It's extension function
is expressed as:

```kotlin
fun observe(): Flow<DataStoreItemChange<out Model>> {
    ...
}
```

A developer may interact with the flow in this way:
```kotlin
Amplify.DataStore.observe()
    .collect { print(it) }
```

API's `subscribe()` also emits a stream of values. However, it is also
important to know about the _lifecycle_ of a GraphQL subscription. So,
this method returns an operation structure, which envelopes two flows:
```kotlin
fun <T> ApiCategory.subscribe(
        request: GraphQLRequest<T>, apiName: String? = null)
        : GraphQLSubscriptionOperation<T> {
    ...
}
```

A developer can inspect the connection state, as well as the stream of
subscription data:
```kotlin
val subscription = Amplify.API.subscribe(request)
subscription.subscriptionData.collect {
    print("Got a subscription data: $it")
}
subscription.connectionState.collect {
    print("Connection state changed: $it")
}
```

The Storage category's various upload and download functions also
exhibit a similar pattern. We want obtain the result, but we may also
like to observe a stream of progress updates. The signature looks like:
```kotlin
fun StorageCategory.downloadFile(
        key: String,
        local: File,
): InProgressStorageOperation<StorageDownloadFileResult> {
```

A developer can access the result of the download via a suspend
function exposed on the download operation:
```kotlin
val result = runBlocking { download.result() }
```

Refer: #605
jamesonwilliams added a commit that referenced this issue Feb 19, 2021
This commit introduces a new, optional `core-kotlin` module.
The module improves the experience of using Amplify from Kotlin.

### Usage
To use the Kotlin facade, include this dependency:
```gradle
dependencies {
    implementation "com.amplifyframework:core-kotlin:$version"
}
```
And import the Kotlin facade *instead of* the similar one at
`com.amplifyframework.core.Amplify`:

```kotlin
import com.amplifyframework.kotlin.core.Amplify
```

### Overview
core-kotlin introduces an alternate `Amplify` facade, which provides new
means of interacting with the various Amplify categories (API, Auth,
DataStore, Predictions, Storage.) The new facade include several
improvements to the Kotlin developer experience, mainly by adding
support for Coroutines.

Amplify has several types of APIs. Some are synchronous calls, which
immediately return a value. However, most Amplify behaviors are
asynchronous calls. Among the async calls, there are a few broad
categories:

 1. Functions that return a single value, and cannot be canceled;
 2. Functions that return a single value, and can be canceled;
 3. Functions that emit a stream of values
 4. Functions that emit multiple types of values

### Single-valued functions

Most Amplify behaviors are of type (1). Auth and Predictions are
entirely comprised of type (1). The Kotlin flavors of these behaviors
are expressed as suspending functions, e.g.:

```kotlin
suspend fun signOut() {
   ...
}
```

The simplest developer experience to invoke this method will be:
```kotlin
activityScope.launch {
    Amplify.Auth.signOut()
}
```

### Cancelable, single-valued functions
Some single-valued functions, such as the `mutate(...)` behavior in the
API category, may be canceled before rendering a result. The Kotlin
version of `mutate(...)` is expressed this way:

```kotlin
suspend fun <R> ApiCategory.mutate(
        request: GraphQLRequest<R>, apiName: String? = null)
        : GraphQLResponse<R> {
    ...
}
```

The user may cancel the behavior via Kotlin's `Job` construct:
```kotlin
val job = activityScope.launch {
    Amplify.API.mutate(request)
}
...
job.cancel()
```

### Multi-valued functions

There are also some Amplify behaviors which emit a stream of values.
DataStore's `observe()` is a canonical example. The Kotlin facade
function is:

```kotlin
suspend fun observe(): Flow<DataStoreItemChange<out Model>> {
    ...
}
```

A developer may interact with the flow in this way:
```kotlin
activityScope.launch {
    Amplify.DataStore.observe()
        .collect { print(it) }
}
```

Note that `observe()` itself is a suspending function. This function
suspends until the observation is ready to be used, e.g.:
```kotlin
val changes: Flow<DataStoreItemChange<out Model>> =
    runBlocking { Amplify.DataStore.observe() }
print("Ready to see DataStore changes!")
activityScope.launch {
    dataStoreChanges.collect { print("Found one: $it") }
}
```

### Mixed-valued functions
API's `subscribe()` also emits a stream of values. Like DataStore, there is
some non-zero cost associated with establishing a subscription. So
`subscribe()` also suspends until it establishes a connection.

```kotlin
suspend fun <T> ApiCategory.subscribe(
        request: GraphQLRequest<T>, apiName: String? = null)
        : Flow<GraphQLResponse<T>> {
    ...
}
```

A developer can access the flow:
```kotlin
val events = runBlocking { Amplify.API.subscribe(request) }
print("Ready to see subscription events!")
activityScope.launch {
    events.collect { print("Found one: $it") }
}
```

The Storage category's various upload and download functions provide
two interesting pieces of information. We want obtain the result of the
transfer, but we may also like to observe a stream of progress updates.
The signature looks like:
```kotlin
fun StorageCategory.downloadFile(
        key: String,
        local: File,
): InProgressStorageOperation<StorageDownloadFileResult> {
```

A developer can access the result of the download via a suspend
function exposed on the download operation:
```kotlin
val result = runBlocking { download.result() }
```
Or get periodic progress updates by doing:
```kotlin
activityScope.async {
    download.progress().collect {
        print("Download made some progress! $it")
    }
}
```

Refer: #605
@taouichaimaa
Copy link

if only you'd make a detailed tutorial, your documentation for android in general is lackluster to say the least. We shouldn't have to suffer to implement MVVM best practices and use Amplify.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
rfc Request for comments
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants