-
Notifications
You must be signed in to change notification settings - Fork 112
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
Comments
I hope amplify can support the coroutine.Now we can use |
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:
In their repo layer they might call something like a login, sign up, query, mutation:
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 |
Thanks @Blu3Parr0t & @shichonghuotian for the feedback! In your replies, I see two unrelated feature requests:
As far as Kotlin support itself is concerned, here are my thoughts. We will produce another "facade" module, like For example, DataStore's vanilla 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:
The API's 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 val job = launch {
val response = mutate(request)
}
job.cancel() Lastly, API's Another important thing to figure out: do we namespace these methods in a new facade, like val response = Amplify.API.mutate(request) |
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
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. |
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
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
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
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
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. |
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:
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!
The text was updated successfully, but these errors were encountered: