Androidで簡易コーポレートサイト（アプリ）を作成する #3 -API連携とバリデーション編-

# 概要
簡易コーポレイトサイトアプリをAndroidで作成していきます。
APIの連携、バリデーション機能の実装を行います。

# API連携
送信ボタンをクリックしてAPIを叩くようにします。

## http通信のための準備
http通信をできるようにするため、ライブラリを追加します。
今回はOkhttp3を使った方法です。

### gradleからインポート
gradleにOkHttp3のライブラリの依存を追加します。
build.gradleを開いて、dependenciesに下記を追加します。
<img width="478" alt="000001.jpg" src="https://dl.dropbox.com/s/49p3enctr6z6772/000001.jpg">

```diff_gradle:build.gradle(Module:AndroidWebsite.app)
	dependencies {
+	    implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.10'
	    implementation 'androidx.core:core-ktx:1.7.0'
	    implementation 'androidx.appcompat:appcompat:1.5.1'
	    implementation 'com.google.android.material:material:1.7.0'
	    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
	    implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
	    implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
	    testImplementation 'junit:junit:4.13.2'
	    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
	    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
	}
```
追加すると上部に「Sync Now」が表示されるので、こちらをクリックして同期させます。
<img width="2445" alt="000010.jpg" src="https://dl.dropbox.com/s/o3fqs2lc98174cn/000010.jpg">


## manifestsに追記
Androidはデフォルトではアプリの通信を許可していないようなので、AndroidManifests.xmlにインターネットへの接続許可を追加します。
src > main > manifests > AndroidManifests.xmlを開いて追加します。

<img width="472" alt="000020.jpg" src="https://dl.dropbox.com/s/9vs1ovou56t9szc/000020.jpg">


```diff_xml:AndroidManifest.xml
	<?xml version="1.0" encoding="utf-8"?>
	<manifest xmlns:android="http://schemas.android.com/apk/res/android"
	    xmlns:tools="http://schemas.android.com/tools">
+	    <uses-permission android:name="android.permission.INTERNET" />
	    <application
	        android:allowBackup="true"
	        android:dataExtractionRules="@xml/data_extraction_rules"
	        android:fullBackupContent="@xml/backup_rules"
	        android:icon="@mipmap/ic_launcher"
	        android:label="@string/app_name"
	        android:roundIcon="@mipmap/ic_launcher_round"
	        android:supportsRtl="true"
	        android:theme="@style/Theme.AndroidWebsite"
	        tools:targetApi="31">
	        <activity
	            android:name=".MainActivity"
	            android:exported="true">
	            <intent-filter>
	                <action android:name="android.intent.action.MAIN" />

	                <category android:name="android.intent.category.LAUNCHER" />
	            </intent-filter>

	            <meta-data
	                android:name="android.app.lib_name"
	                android:value="" />
	        </activity>
	    </application>

	</manifest>
```

## ContactFragment.kt編集
ボタンをクリックしたらAPIを叩くように実装します。


「app」> 「java」 > 「come.example.androidwebsite」 で　ContactFrament.ktを開いて編集します。（設定よって表記が微妙に異なり.ktの拡張子が隠れていることもあります）
<img width="490" alt="000030.jpg" src="https://dl.dropbox.com/s/qft5mwnhnwyprzb/000030.jpg">

onCreateView()でそれぞれの値を取得します。
また、それぞれの値の初期化も追加します。

```diff_kotlin:ContactFragment.kt
    class ContactFragment : Fragment() {

+	    private var nameEditText: EditText? = null
+	    private var emailEditText: EditText? = null
+	    private var bodyEditText: EditText? = null
+	    private lateinit var sendButton: Button

	    override fun onCreate(savedInstanceState: Bundle?) {
	        super.onCreate(savedInstanceState)
	        arguments?.let {

	        }
	    }

        override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {

-           return inflater.inflate(R.layout.fragment_contact, container, false)
+           val view = inflater.inflate(R.layout.fragment_contact, container, false)
+           val nameEditText = view.findViewById<EditText>(R.id.etName)
+           val emailEditText = view.findViewById<EditText>(R.id.etEmail)
+           val bodyEditText = view.findViewById<EditText>(R.id.etBody)
+            val sendButton = view.findViewById<Button>(R.id.button)

+           return view

        }
```

```kotlin:コピペ用
        private var nameEditText: EditText? = null
        private var emailEditText: EditText? = null
        private var bodyEditText: EditText? = null
        private lateinit var sendButton: Button
```
```kotlin:コピペ用
        val view = inflater.inflate(R.layout.fragment_contact, container, false)
        val nameEditText = view.findViewById<EditText>(R.id.etName)
        val emailEditText = view.findViewById<EditText>(R.id.etEmail)
        val bodyEditText = view.findViewById<EditText>(R.id.etBody)
        val sendButton = view.findViewById<Button>(R.id.button)

        return view
```

赤文字エラーはカーソルをあわせて「Import」を実行してエラーを解決します。
clickイベントを設定していきます。
ボタンをクリックしたら、ログに押されたと表示されるようにします。

```diff_kotlin:ContactFragment.kt
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        val view = inflater.inflate(R.layout.fragment_contact, container, false)
        val nameEditText = view.findViewById<EditText>(R.id.etName)
        val emailEditText = view.findViewById<EditText>(R.id.etEmail)
        val bodyEditText = view.findViewById<EditText>(R.id.etBody)
        val sendButton = view.findViewById<Button>(R.id.btn)

+       sendButton.setOnClickListener {
+           Log.d("click","押された")
+       }

        return view

    }
```
```kotlin:コピペ用
sendButton.setOnClickListener {
    Log.d("click","押された")
}
```

エミュレータを起動して、送信ボタンをクリックしてみます。
ログに「押された」と表示されているので動作しているようです。
<img width="2445" alt="000040.jpg" src="https://dl.dropbox.com/s/4m1hnjhyy7fgd2v/000040.jpg">


### パラメータ準備
okHttpクライアントを作成します。
メティアタイプの設定と、APIのURLの指定もします。

```diff_kotlin:ContactFragment.kt
    package com.example.simpleweb

    import android.os.Bundle
    import android.util.Log
    import androidx.fragment.app.Fragment
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import android.widget.Button
    import android.widget.EditText
    import okhttp3.MediaType.Companion.toMediaType
    import okhttp3.OkHttpClient

    // TODO: Rename parameter arguments, choose names that match
    // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER


    /**
     * A simple [Fragment] subclass.
     * Use the [ContactFragment.newInstance] factory method to
     * create an instance of this fragment.
     */
    class ContactFragment : Fragment() {
        // TODO: Rename and change types of parameters
+       private val client = OkHttpClient.Builder().build()
+       private val MEDIA = "application/x-www-form-urlencoded; charset=utf-8".toMediaType()
+       private val urlStr = "https://script.google.com/macros/s/デプロイID/exec"
        
        private var nameEditText: EditText? = null
        private var emailEditText: EditText? = null
        private var bodyEditText: EditText? = null
        private lateinit var sendButton: Button

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)

        }

        override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {

            val view = inflater.inflate(R.layout.fragment_contact, container, false)
            nameEditText = view.findViewById(R.id.etName)
            emailEditText = view.findViewById(R.id.etEmail)
            bodyEditText = view.findViewById(R.id.etBody)
            sendButton = view.findViewById(R.id.btn)

            sendButton.setOnClickListener {
                Log.d("click","押された")

            }

            return view

        }

        companion object {
            /**
             * Use this factory method to create a new instance of
             * this fragment using the provided parameters.
             *
             * @param param1 Parameter 1.
             * @param param2 Parameter 2.
             * @return A new instance of fragment ContactFragment.
             */
            // TODO: Rename and change types and number of parameters
            @JvmStatic
            fun newInstance(param1: String, param2: String) =
                ContactFragment().apply {
                    arguments = Bundle().apply {

                    }
                }
        }
    }
```
```kotlin:コピペ用
private val client = OkHttpClient.Builder().build()
private val MEDIA = "application/x-www-form-urlencoded; charset=utf-8".toMediaType()
private val urlStr = "https://script.google.com/macros/s/デプロイID/exec"
```
赤文字エラーはImportで解決します。

ログを表示しているところでAPI送信を実行します。
送信データを作成し、リクエストを作成します。
送信を実行し、レスポンスが正常に返ってきたら、Logにレスポンスを表示させます。
エラーの場合も設定しておきます。
> こちらも赤文字はImportで解決しますが、Importの選択肢が複雑なので、下記コードの上部のimportを確認しながらImport実行するか、importのコードもコピーして貼り付けてしまってもいいかと思います。

```diff_kotlin:ContactFragment.kt
package com.example.simpleweb

import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.IOException

//~~~~省略~~~~~~~

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        val view = inflater.inflate(R.layout.fragment_contact, container, false)
        nameEditText = view.findViewById(R.id.etName)
        emailEditText = view.findViewById(R.id.etEmail)
        bodyEditText = view.findViewById(R.id.etBody)
        sendButton = view.findViewById(R.id.btn)

        sendButton.setOnClickListener {
+           val sendData = "name="+nameEditText?.text.toString()+"&&email="+emailEditText?.text.toString()+"&&body="+bodyEditText?.text.toString()
+           val request = Request.Builder()
+               .url(urlStr)
+               .post(sendData.toRequestBody(MEDIA))
+               .build()
+
+           client.newCall(request).enqueue(object : Callback {
+
+               override fun onResponse(call: Call, response: Response) {
+                   val responseBody = response.body?.string().orEmpty()
+                   val responseBodyJson = JSONObject(responseBody)
+                   Handler(Looper.getMainLooper()).post {
+                       Log.d("レスポンス", responseBodyJson.getString("message"))
+                   }
+
+               }
+
+               override fun onFailure(call:Call,e: IOException){
+                   Log.e("ERROR",e.toString())
+               }
+           })
        }

        return view

    }

```
```kotlin:コピペ用
val sendData = "name="+nameEditText?.text.toString()+"&&email="+emailEditText?.text.toString()+"&&body="+bodyEditText?.text.toString()
val request = Request.Builder()
    .url(urlStr)
    .post(sendData.toRequestBody(MEDIA))
    .build()

client.newCall(request).enqueue(object : Callback {

    override fun onResponse(call: Call, response: Response) {
        val responseBody = response.body?.string().orEmpty()
        val responseBodyJson = JSONObject(responseBody)
        Handler(Looper.getMainLooper()).post {
            Log.d("レスポンス", responseBodyJson.getString("message"))
        }

    }

    override fun onFailure(call:Call,e: IOException){
        Log.e("ERROR",e.toString())
    }
})
```

## 確認
エミュレータを起動して、それぞれ正しい値を入力して実行してみます。
ログにレスポンスが返ってきているのが確認できます。
「success!」はAPI側から返ってきているのできちんとAPI連携できたことがわかります。
<img width="2445" alt="000050.jpg" src="https://dl.dropbox.com/s/5yxzrjd5qdrc2le/000050.jpg">


## トースト
レスポンスをトーストで表示させます。下記のように書き換えます。

```diff_kotlin:ContactFragment.kt
import android.widget.Toast

//~~~省略~~~~

    client.newCall(request).enqueue(object : Callback {

        override fun onResponse(call: Call, response: Response) {
            val responseBody = response.body?.string().orEmpty()
            val responseBodyJson = JSONObject(responseBody)
            Handler(Looper.getMainLooper()).post {
-               Log.d("レスポンス", responseBodyJson.getString("message"))
+               Toast.makeText(requireContext(),responseBodyJson.getString("message"),Toast.LENGTH_SHORT).show()
            }

        }

        override fun onFailure(call: Call, e: IOException) {
            Log.e("ERROR", e.toString())
        }
    })

```
確認するとふわっと表示されているのが確認できます。
<img width="2137" alt="000060.jpg" src="https://dl.dropbox.com/s/53zrpcsa62dvi9e/000060.jpg">


# バリデーション
バリデーションを設定していきます。
それぞれエラーメッセージの表示まではできているので、入力中にエラーメッセージの出し分けをするようにします。
まず、それぞれのエラーメッセージを取得します。

```diff_kotlin:ContactFragment.kt
+  import android.widget.TextView

//~~~省略~~~~

    override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {

            val view = inflater.inflate(R.layout.fragment_contact, container, false)
            val nameEditText = view.findViewById<EditText>(R.id.etName)
            val emailEditText = view.findViewById<EditText>(R.id.etEmail)
            val bodyEditText = view.findViewById<EditText>(R.id.etBody)
            val sendButton = view.findViewById<Button>(R.id.btn)
            
+           val nameError = view.findViewById<TextView>(R.id.nameErr)
+           val emailError = view.findViewById<TextView>(R.id.emailErr)
+           val bodyError = view.findViewById<TextView>(R.id.bodyErr)
```
```kotlin:コピペ用
val nameError = view.findViewById<TextView>(R.id.nameErr)
val emailError = view.findViewById<TextView>(R.id.emailErr)
val bodyError = view.findViewById<TextView>(R.id.bodyErr)
```

## name必須バリデーション
名前は必須のチェックのみを実装します。
nameEditTextにsetOnKeyListenerを設定し、キーイベントでnameEditTextの値がからかどうかを判断。
emptyの場合はエラーメッセージにvisibilityをVISBLE、それ以外はINVISIBLEにします。

```diff_kotlin:ContactFragment.kt

        override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {

            val view = inflater.inflate(R.layout.fragment_contact, container, false)
            val nameEditText = view.findViewById<EditText>(R.id.etName)
            val emailEditText = view.findViewById<EditText>(R.id.etEmail)
            val bodyEditText = view.findViewById<EditText>(R.id.etBody)
            val sendButton = view.findViewById<Button>(R.id.btn)

            val nameError = view.findViewById<TextView>(R.id.nameErr)
            val emailError = view.findViewById<TextView>(R.id.emailErr)
            val bodyError = view.findViewById<TextView>(R.id.bodyErr)
            
+           nameEditText.apply { 
+               setOnKeyListener{_, keyCode, keyEvent ->
+                   if(nameEditText.text.toString().isEmpty()){
+                       nameError.visibility = View.VISIBLE
+                   }else{
+                       nameError.visibility = View.INVISIBLE
+                   }
+                   return@setOnKeyListener false
+               }
+           }

            sendButton.setOnClickListener {

```

```kotlin:コピペ用
nameEditText.apply { 
    setOnKeyListener{_, keyCode, keyEvent ->
        if(nameEditText.text.toString().isEmpty()){
            nameError.visibility = View.VISIBLE
        }else{
            nameError.visibility = View.INVISIBLE
        }
        return@setOnKeyListener false
    }
}
```


## emailバリデーション
emailはバリデーションパターンを正規表現で設定しておきます。
それを、name同様にsetOnKeyListernerでmatchするか判断してエラー文言の出し分けをします。

```diff_kotlin:ContactFragment.kt
+       val PARAM_EMAIL = Regex("^[a-z0-9.]+@[a-z0-9.]+\\.[a-z]+\$")

        nameEditText.apply {
            setOnKeyListener{_, keyCode, keyEvent ->
                if(nameEditText.text.toString().isEmpty()){
                    nameError.visibility = View.VISIBLE
                }else{
                    nameError.visibility = View.INVISIBLE
                }
                return@setOnKeyListener false
            }
        }

+       emailEditText.apply {
+           setOnKeyListener { _, keyCode, keyEvent ->
+               if(emailEditText.text.toString().matches(PARAM_EMAIL)){
+                   emailError.visibility = View.INVISIBLE
+               }else{
+                   emailError.visibility = View.VISIBLE
+               }
+               return@setOnKeyListener false
+           }
+       }

        sendButton.setOnClickListener {
```

```kotlin:コピペ用
emailEditText.apply {
    setOnKeyListener { _, keyCode, keyEvent ->
        if(emailEditText.text.toString().matches(PARAM_EMAIL)){
            emailError.visibility = View.INVISIBLE
        }else{
            emailError.visibility = View.VISIBLE
        }
        return@setOnKeyListener false
    }
}
```

## Bodyバリデーション
BodyもEmail同様に。

```diff_kotlin:ContactFragment.kt
        val PARAM_EMAIL = Regex("^[a-z0-9.]+@[a-z0-9.]+\\.[a-z]+\$")
+       val PARAM_BODY = Regex("^.{1,10}\$")

        nameEditText.apply {
            setOnKeyListener{_, keyCode, keyEvent ->
                if(nameEditText.text.toString().isEmpty()){
                    nameError.visibility = View.VISIBLE
                }else{
                    nameError.visibility = View.INVISIBLE
                }
                return@setOnKeyListener false
            }
        }

        emailEditText.apply {

            setOnKeyListener { _, keyCode, keyEvent ->
                if(emailEditText.text.toString().matches(PARAM_EMAIL)){
                    emailError.visibility = View.INVISIBLE
                }else{
                    emailError.visibility = View.VISIBLE
                }
                return@setOnKeyListener false
            }
        }

+       bodyEditText.apply {
+           setOnKeyListener { _, keyCode, keyEvent ->
+               if(bodyEditText.text.toString().matches(PARAM_BODY)){
+                   bodyError.visibility = View.INVISIBLE
+               }else{
+                   bodyError.visibility = View.VISIBLE
+               }
+               return@setOnKeyListener false
+           }
+       }
```
```kotlin:コピペ用
bodyEditText.apply {
    setOnKeyListener { _, keyCode, keyEvent ->
        if(bodyEditText.text.toString().matches(PARAM_BODY)){
            bodyError.visibility = View.INVISIBLE
        }else{
            bodyError.visibility = View.VISIBLE
        }
        return@setOnKeyListener false
    }
}
```

## ボタン
エラーが出ている間はAPI送信しないように、エラーが出ているときはトーストで「入力内容を確認してください。」と表示させるようにします。

```diff_kotlin:ContactFragment.kt
        sendButton.setOnClickListener {
+           if(bodyError.visibility == View.INVISIBLE && nameError.visibility == View.INVISIBLE && emailError.visibility == View.INVISIBLE) {
                val sendData =
                    "name=" + nameEditText?.text.toString() + "&&email=" + emailEditText?.text.toString() + "&&body=" + bodyEditText?.text.toString()
                val request = Request.Builder()
                    .url(urlStr)
                    .post(sendData.toRequestBody(MEDIA))
                    .build()

                client.newCall(request).enqueue(object : Callback {

                    override fun onResponse(call: Call, response: Response) {
                        val responseBody = response.body?.string().orEmpty()
                        val responseBodyJson = JSONObject(responseBody)
                        Handler(Looper.getMainLooper()).post {
                            Toast.makeText(requireContext(),responseBodyJson.getString("message"),Toast.LENGTH_SHORT).show()
                        }
                    }

                    override fun onFailure(call: Call, e: IOException) {
                        Log.e("ERROR", e.toString())
                    }
                })
+           }else{
+               Toast.makeText(requireContext(),"入力内容を確認してください。",Toast.LENGTH_SHORT).show()
+           }
        }
```
```kotlin:コピペ用
sendButton.setOnClickListener {
    if(bodyError.visibility == View.INVISIBLE && nameError.visibility == View.INVISIBLE && emailError.visibility == View.INVISIBLE) {
        val sendData = "name="+nameEditText?.text.toString()+"&&email="+emailEditText?.text.toString()+"&&body="+bodyEditText?.text.toString()
        val request = Request.Builder()
            .url(urlStr)
            .post(sendData.toRequestBody(MEDIA))
            .build()

        client.newCall(request).enqueue(object : Callback {

            override fun onResponse(call: Call, response: Response) {

                val responseBody = response.body?.string().orEmpty()
                val responseBodyJson = JSONObject(responseBody)
                Handler(Looper.getMainLooper()).post {
                    Toast.makeText(requireContext(),responseBodyJson.getString("message"),Toast.LENGTH_SHORT).show()
                }

            }

            override fun onFailure(call:Call,e: IOException){
                Log.e("ERROR",e.toString())
            }
        })
    }else{
        Toast.makeText(requireContext(),"入力内容を確認してください。",Toast.LENGTH_SHORT).show()
    }
}
```

## 動作確認
エミュレータを起動し、入力中にエラーメッセージが表示されるか、送信ボタンを押したら期待通りトーストが表示されるかを確認します。
<img width="2137" alt="000070.jpg" src="https://dl.dropbox.com/s/ntkirbsu68ojqqi/000070.jpg">
また適切な値を入力するとエラーメッセージが消えることも確認します。
<img width="2137" alt="000080.jpg" src="https://dl.dropbox.com/s/zq4pyq2l0sgh6os/000080.jpg">

これで全機能の実装が完了しました。

# 関連コンテンツ
* 前：[Androidで簡易コーポレートサイト（アプリ）を作成する #2 -TOP画面とContact画面編-](https://qiita.com/minimumskills/private/c39c04dec69a0021d96c)
* [超シンプルなWeb（アプリ）作成を通して各種フロントエンド技術を体験する - 2023 Early -](https://qiita.com/minimumskills/private/9e7efe86e244bb9844b0)