[動画投稿] Android + AWS AmplifyでAWSに入門してみた話 #devio2020

Developers.IO 2020 Connectで「Android + AWS AmplifyでAWSに入門してみた!」についてビデオセッションを公開しました。セッション動画では省略してしまったソースコードや解説などを追記しております。
2020.06.23

みなさん、Developers.IO 2020 楽しんでますか?(挨拶 新卒エンジニアのハウンです🐑

2020年6月16日(火)より、弊社主催のオンラインイベントDEVELOPERS.IO 2020 CONNECTが開催されています。本日はUX系のセッションが勢ぞろいとなっていることで、私もビデオセッションを公開いたしました!

新卒エンジニアであり、韓国人という身分では過酷な挑戦だったことではあったものの、周りからも励ましていただいて最後まで頑張ることができたと思います。

本記事では「Android + AWS AmplifyでAWSに入門してみた!」のセッションについて振り返ります。

セッション動画

「AWSよくわからないけど、とりあえずアプリ開発に捗るものを使ってみたい!」と思った過去の私がAmplifyを試してみた方法を紹介しています。

Android + Amplify をやってみようと思ったきっかけ

学生時代、プロジェクトでアプリ開発をやってた頃、こんな悩みで毎晩うなされていました。(オーバーな表現してますけど、私にとっては本当に辛い思い出でした。)バックエンドは構築できなくても、さすがに未完成のものを提出するわけにはいかなかったので、Firebaseを最後の手段として使ってました?

もっと楽にバックエンド構築だできたらな。。。と思っていた私にとってAmplifyは画期的であり、Androidと繋げることができるということで挑戦してみました。

今回は、とりあえず一番よく使いそうな認証とデータの部分まで試しています。

開発環境

動画でも一度触れておいた内容ですが、まだご覧にならなかった方のためにもう一度残しておきます。

  • Windows10
  • Node.js バージョン 10.x 以上
  • Android Studio 3.6 以上
  • Android SDK バージョン 16 (Android 4.1) 以上
  • AWS Management Console アカウント
  • AndroidプロジェクトとVirtual Deviceを作成しておくこと

あと、Amplify CLIでのユーザー設定やプロジェクト作成などは動画内で説明しておりますので、ご確認ください。

ソースコード

セッション動画ではソースコードまで説明すると時間が長引きそうだったので省略させていただきました。

セッションの内容を実際に試してみたい方もいらっしゃると思い、こちらにソースコードと解説を残しておきます。

Auth追加パート

  • AndroidManifest.xml にインターネット、ネットワーク接続を許可するコード追加
<uses-permission android:name = "android.permission.INTERNET"/>
<uses-permission android:name = "android.permission.ACCESS_NETWORK_STATE"/>

 

  • build.gradle (:app) に aws mobile client, auth, cognitoauth SDK を追加

動画での説明順とは異なりますが、こちらを先に追加しておくとAuthActivityの作成が楽になります。

// Mobile Client for initializing the SDK
implementation('com.amazonaws:aws-android-sdk-mobile-client:2.8.+@aar') { transitive = true }

// Cognito UserPools for SignIn
implementation('com.amazonaws:aws-android-sdk-auth-userpools:2.8.+@aar') { transitive = true }

// Sign in UI Library
implementation('com.amazonaws:aws-android-sdk-auth-ui:2.8.+@aar') { transitive = true }

dependenciesの中に追加するとSync Nowという文字が表示されるのでクリックして処理を待ちます。

 

  • Auth UI を利用するための Activity 追加

最初作成したプロジェクトにはMainActivityしかありません。MainActivityはAPI追加の検証で使用しますので、AuthActivityを新しく作成します。

public class AuthActivity extends AppCompatActivity {

    private final String TAG = AuthActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_auth);

        AWSMobileClient.getInstance().initialize(getApplicationContext(), new Callback<UserStateDetails>() {

            @Override
            public void onResult(UserStateDetails userStateDetails) {
                Log.i(TAG, userStateDetails.getUserState().toString());
                switch (userStateDetails.getUserState()){
                    case SIGNED_IN:
                        Intent i = new Intent(AuthActivity.this, MainActivity.class);
                        startActivity(i);
                        break;
                    case SIGNED_OUT:
                        showSignIn();
                        break;
                    default:
                        AWSMobileClient.getInstance().signOut();
                        showSignIn();
                        break;
                }
            }

            @Override
            public void onError(Exception e) {
                Log.e(TAG, e.toString());
            }
        });
    }

    private void showSignIn() {
        try {
            AWSMobileClient.getInstance().showSignIn(this,
                    SignInUIOptions.builder().nextActivity(MainActivity.class).build());
        } catch (Exception e) {
            Log.e(TAG, e.toString());
        }
    }
}

ソースコードを見ていただきますと、userStateDetailsというインスタンスにユーザー認証状態に関する情報を一時的に保存していることになっています。そして、その情報をもとにswitch-case文で表示する画面を制御しています。

API追加パート

  • activity_main.xml 修正
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/addText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button" />

    <ListView
        android:id="@+id/TestList"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"/>

</LinearLayout>

 

  • build.gradle (Project)の build:gradle バージョンを3.2.0に修正、appsync build plugin を追加

こちらもソースコードを追加する前に、build.gradleを先に修正しておくと楽になりますので順番を変えました。

dependencies {
classpath "com.android.tools.build:gradle:3.2.0"

classpath 'com.amazonaws:aws-android-sdk-appsync-gradle-plugin:2.6.+'

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}

最新のバージョンのAndroid Studioからプロジェクトを作成すると、build:gradleバージョンが4以上であることを確認できるはずです。ですが、その状態でappsync-gradle-pluginを追加しようとするとうまく認識できない問題が起こります。

gradleバージョンを3.2.0に下げると、プラグインを認識するようになります。

 

  • build.gradle (:app) に appsync client SDK とプラグインを追加

まず、apply plugin: 'com.android.application' と書かれている次の行に下記のコードを追加する必要があります。

apply plugin: 'com.amazonaws.appsync'

あとはdependenciesにAppSync Client SDKを追加します。

//For AWSAppSyncClient:
implementation 'com.amazonaws:aws-android-sdk-appsync:3.0.1'

 

  • AppSync を使用するために AppSync Client クラスを作成
public class ClientTest {
    private static volatile AWSAppSyncClient client;

    public static synchronized void init(final Context context) {
        if (client == null) {
            final AWSConfiguration awsConfiguration = new AWSConfiguration(context);
            client = AWSAppSyncClient.builder()
                    .context(context)
                    .awsConfiguration(awsConfiguration)
                    .cognitoUserPoolsAuthProvider(new CognitoUserPoolsAuthProvider() {
                        @Override
                        public String getLatestAuthToken() {
                            try {
                                return AWSMobileClient.getInstance().getTokens().getIdToken().getTokenString();
                            } catch (Exception e){
                                Log.e("APPSYNC_ERROR", e.getLocalizedMessage());
                                return e.getLocalizedMessage();
                            }
                        }
                    }).build();
        }
    }

    public static synchronized AWSAppSyncClient appSyncClient() {
        return client;
    }
}

こちらのコードはAuth追加段階で作成されたUserPoolをもとにAppSyncの認証を行うようになっています。

 

  • MainActivityにテキストを追加すると DynamoDB に保存、データを読み込むコードを作成

理解しやすいようにDynamoDBに直接接続してるように表現してますが、実際のコードはAppSync側でDynamoDBにリクエストを送りレスポンスを受ける感じになっています。

public class MainActivity extends AppCompatActivity {

    ListView mListView;
    Button mButton;
    EditText mEditText;

    private ArrayList<ListTextsQuery.Item> mText;
    private ArrayAdapter<ListTextsQuery.Item> Adapter;
    private final String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mListView = findViewById(R.id.TestList);
        mButton = findViewById(R.id.addText);
        mEditText = findViewById(R.id.editText);

        ClientTest.init(this);

        mButton.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                save();

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                query();
            }
        });
    }

    @Override
    protected void onStart() {
        super.onStart();

        // Query list data when we start application
        query();
    }

    //データ読み込み
    public void query() {
        ClientTest.appSyncClient().query(ListTextsQuery.builder().build())
                .responseFetcher(AppSyncResponseFetchers.CACHE_AND_NETWORK)
                .enqueue(queryCallback);
    }

   // Query callback code
   private GraphQLCall.Callback<ListTextsQuery.Data> queryCallback = new GraphQLCall.Callback<ListTextsQuery.Data>() {
       @Override
       public void onResponse(@Nonnull Response<ListTextsQuery.Data> response) {

           mText = new ArrayList<>(response.data().listTexts().items());

           Log.i(TAG, "Retrieved list items: " + mText.toString());

           Adapter = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_list_item_1, mText);

           runOnUiThread(new Runnable() {
               @Override
               public void run() {
                   mListView.setAdapter(Adapter);
                   Toast.makeText(MainActivity.this, "Print text", Toast.LENGTH_SHORT).show();
               }
           });
       }

       @Override
       public void onFailure(@Nonnull ApolloException e) {
           Log.e(TAG, e.toString());
       }
   };

   // データ保存
   private void save() {
            final String text = mEditText.getText().toString();

            CreateTextInput input = CreateTextInput.builder().text(text).build();
            CreateTextMutation addTextMutation = CreateTextMutation.builder().input(input).build();

            ClientTest.appSyncClient().mutate(addTextMutation).enqueue(mutateCallback);

            mEditText.setText("");
   }

   // Mutation callback code
   private GraphQLCall.Callback<CreateTextMutation.Data> mutateCallback = new GraphQLCall.Callback<CreateTextMutation.Data>() {
       @Override
       public void onResponse(@Nonnull final Response<CreateTextMutation.Data> response) {
           runOnUiThread(new Runnable() {
               @Override
               public void run() {
                   Toast.makeText(MainActivity.this, "Added text", Toast.LENGTH_SHORT).show();
               }
           });
       }

       @Override
       public void onFailure(@Nonnull final ApolloException e) {
           runOnUiThread(new Runnable() {
               @Override
               public void run() {
                   Log.e("", "Failed to perform AddPetMutation", e);
                   Toast.makeText(MainActivity.this, "Failed to add pet", Toast.LENGTH_SHORT).show();
                   MainActivity.this.finish();
               }
           });
       }
   };
}

つまり、save()はデータを保存するようにとリクエストしてレスポンスを返してもらい、query()はデータを読み込むリクエストをして成功すると保存されているデータをレスポンスとして返してもらうかたちとなります。

あと、onclick関数の中にsleep(1000);を使用している理由はsave()が反映されるのに少し時間がかかるからです。(元々そうなのか、ソースコードの問題かは検証の必要がありそうですね。)

ひとまず、実行に必要な作業はここまでになります。

実行するとエラーが起きる場合

実行した時に 'Your project may be using a third-party plugin which is not compatible with the other plugins in the project or the version of Gradle requested by the project.' が表示されてビルドができなくなる場合があります。この原因もgradle側から影響しています。

この場合、gradle-wrapper.propertiesファイルのdistributionUrlを以下のように修正します。

distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip

修正後、Sync Nowをするとエラーが消えることを確認できます。

最後に

実はこのセッションを準備する前はAWSのサービスの理解自体があまりできてなかったもので、間に合わなかったらどうしようと心配になったりもしました。動画編集も最後やったのが5年前ぐらいだったので、久々に大変な思いをしましたね...?

けれど、その分毎日新しい挑戦ができていることにもなっているので、とても充実しています! (周りから受ける刺激が半端ない)

来年はもっと多くの方に役立つテーマで登壇できたらなと思います。