Android(Kotlin) WebサーバーにPOSTでUpload と フチベニベンケイ
今日の朝の散歩です。だれかお友達が来ないか一生懸命目を凝らしている柴犬です。
近くの公園の桜の木です。この木は毎年3月の10日頃咲きますので、今の時期は蕾ですが来週はちらほら綺麗な花が咲き始めていると思います。
来週、また様子を見に行ってきます。この時も柴犬も一緒でしたがアングル的無理でした。
概要
プログラム言語「Kotlin」を使ってWEBサーバーにHTTPのPOSTを使ってアップロードを考えてみました。
Android Studioの「logcat」で次の画像の結果まで達成できましたので記録することにします。
1冊のみでは理解に苦しいところがあるので更に新しく本を購入しました。
2024年2月26日現在
WEBのみでは断片的で覚えにくいので最初に購入した Kotlin の本です。
AndroidアプリのWEBの記事も Kotlin が過半数となっているのかなと感じます。CameraX の記事に至っては、私の捜査が悪いこともありますが、これまでJAVAで動作する記事に私は遭遇したことがありません。
フチベニベンケイ
1月27日から1カ月後の様子を記録しました。
赤5は相変わらず葉っぱのままです。芽が出てくる様子はありません。
ほかのものは順調に成長しています。
AndroidManifest.xml
ネットに接続しますので次の permission の追記が必要です。
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
ネットワークの security の警告が AndroidStudio の logcat に出ましたので次のコードを追加しました。
android:networkSecurityConfig="@xml/network_security_config"
全体は次のようになりました。
<?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" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <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.Jsonuploadkotlin" android:networkSecurityConfig="@xml/network_security_config" 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>
build.gradle.kts(:app)
viewBinding を使いますので次のコードを追記します。
buildFeatures {
viewBinding = true
}
JSON を扱いますので次の依存関係を追加します。
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
全体は次のようになりました。
plugins { id("com.android.application") id("org.jetbrains.kotlin.android") } android { namespace = "org.sibainu.relax.room.jsonuploadkotlin" compileSdk = 34 defaultConfig { applicationId = "org.sibainu.relax.room.jsonuploadkotlin" minSdk = 26 targetSdk = 33 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } buildFeatures { viewBinding = true } } dependencies { implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.11.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") }
res/layout/activity_main.xml
ガイドラインを使用しましたので文字が大分多くなりました。
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 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" tools:context=".MainActivity"> <Button android:id="@+id/upload_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="15dp" android:text="Button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView2" /> <TextView android:id="@+id/textView1" android:layout_width="0dp" android:layout_height="wrap_content" android:text="TextView" app:layout_constraintEnd_toStartOf="@+id/guideline" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/guideline3" /> <TextView android:id="@+id/textView2" android:layout_width="0dp" android:layout_height="19dp" android:text="textView2" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="@+id/guideline" app:layout_constraintTop_toTopOf="@+id/guideline3" app:layout_constraintVertical_bias="0.0" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" app:layout_constraintGuide_percent="0.3" /> <androidx.constraintlayout.widget.Barrier android:layout_width="wrap_content" android:layout_height="wrap_content" app:barrierDirection="top" /> <androidx.constraintlayout.widget.Barrier android:layout_width="wrap_content" android:layout_height="wrap_content" app:barrierDirection="left" /> <androidx.constraintlayout.widget.Barrier android:layout_width="wrap_content" android:layout_height="wrap_content" app:barrierDirection="top" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.3" /> </androidx.constraintlayout.widget.ConstraintLayout>
こんな感じのレイアウトになります。
res/xml/network_security_config.xml
ネットワークの security の警告が AndroidStudio の logcat に出ましたので次のコードを追加しました。
AndroidManifest.xml にこのファイルの参照コードを追記します。
<?xml version="1.0" encoding="utf-8"?> <network-security-config> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="true">https://サーバー</domain> </domain-config> </network-security-config>
MainActivity.kt
関数、クラスをブロック毎に掲示して最後に全体のコードを掲載しています。
fun onCreate
クラス ClickListener を作成して登録しています。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewBinding = ActivityMainBinding.inflate(layoutInflater) setContentView(viewBinding.root) findViewById<Button>(R.id.upload_button).setOnClickListener(ClickListener()) }
fun receiveInfo
ボタンをクリックして呼ばれる関数です。
@UiThread private fun receiveInfo(postUrl: String, bodyData: String) { val backgroundReceiver = InfoBackground(postUrl, bodyData) val executeService = Executors.newSingleThreadExecutor() Log.d(TAG, "receiveInfo executeService :" + bodyData) // サービスを作成して送信を実行、結果を受けます。 val future = executeService.submit(backgroundReceiver) Log.d(TAG, "receiveInfo future :" + bodyData) // 結果を取得します。 val result = future.get() if (result != "") { showInfo(result) } else { // 失敗のときの JSON を作成します。 showInfo("{\"username\":\"失敗しました\"}") } }
fun showInfo
@UiThread private fun showInfo(result: String) { Log.d(TAG, "showInfo :" + result) // レスポンスJSONオブジェクトを生成。 val rootJSON = JSONObject(result) // try catch で処理すべきかも。 rootJSON?.apply { val userName = rootJSON.getString("username") Log.d(TAG, "ユーザー名 :" + userName) val telop = "ユーザー名:${userName}" val tvTelop = findViewById<TextView>(R.id.textView2) tvTelop.text = telop } }
fun requestBodyData
HTTP の POST 送信のリクエストのボディ部分を作成する関数です。
正しい形式が分かりませんでしたので取り合えずという状態です。使うとなれば現実に合わせて修正しなければなりません。
private fun requestBodyData(username: String, userid: String, access: String, key: String, jsondata: Map<Any?,Any?>): String { val postmap: MutableMap<Any?, Any?> = mutableMapOf("username" to username) postmap.put("userid", userid) postmap.put("access", access) postmap.put("key", key) postmap.put("data", jsondata) return JSONObject(postmap).toString() }
inner class InfoBackground
private inner class InfoBackground(postUrl: String, bodyData: String): Callable<String> { // 引数 postData、bodyData を call の中で直接使えないので // メンバーを作ります。 val _postUrl = postUrl val _bodyData = bodyData @WorkerThread override fun call(): String { // POSTデータ。 val postData = _bodyData // 送信結果が格納される。 var result = "" // URLオブジェクトを生成。 val url = URL(_postUrl) // URLオブジェクトからHttpURLConnectionオブジェクトを取得。 val con = url.openConnection() as HttpURLConnection // 接続に使ってもよい時間を設定。 con.connectTimeout = 1000 // データ取得に使ってもよい時間。 con.readTimeout = 1000 // HTTP接続メソッドをPOSTに設定。 con.requestMethod = "POST" con.doOutput = true // データのタイプを設定。 con.setRequestProperty("Content-type", "application/json; charset=utf-8") // キャッシュは使いません。 con.useCaches = false Log.d(TAG, "InfoBackground" + _bodyData) try { // 接続。 con.connect() Log.d(TAG, "connect :" + _bodyData) // アップロード val os = con.outputStream os?.run { // 送信するデータをバイト配列に変換 val postDataBytes = postData.toByteArray(charset("UTF-8")) // アップロードの実行 write(postDataBytes) // OnputStreamオブジェクトを解放。 flush() close() // レスポンスコードを取得。 val statusCode = con.responseCode Log.d(TAG, "statuscode :" + statusCode.toString()) if (statusCode == HttpURLConnection.HTTP_OK) { // レスポンスデータであるInputStreamオブジェクトを文字列に変換。 val stream = con.inputStream result = is2String(stream) } } Log.d(TAG, "inputstream :" + _bodyData) } catch(ex: SocketTimeoutException) { Log.w(TAG, "通信タイムアウト", ex) } finally { con.disconnect() } Log.d(TAG, "result :" + result) // 成功なら JSON 文字列を返し、失敗のときは空文字。 return result } private fun is2String(stream: InputStream): String { val sb = StringBuilder() val reader = BufferedReader(InputStreamReader(stream, StandardCharsets.UTF_8)) var line = reader.readLine() while(line != null) { sb.append(line) line = reader.readLine() } reader.close() return sb.toString() } }
companion object
Kotlinでは、クラス内の static な定数は companion object の中に定義するのが一般的のようですので、その例に習いました。
companion object { private const val TAG = "AsyncSample" private const val POSTURL = "https://サーバー/script.php" private const val USERNAME = "name" private const val USERID = "userid" private const val ACCESS = "access" private const val KEY = "key" }
inner class ClickListener
共通の Listener のクラスを作成しました。View のIDで場合分けをしています。
private inner class ClickListener: View.OnClickListener { override fun onClick(v: View?) { when (v?.id) { R.id.upload_button -> { val dataMap: MutableMap<Any?, Any?> = mutableMapOf("field1" to "hoge hoge") dataMap.put("field2", "1234567890") dataMap.put("field3", "root") dataMap.put("field4", "abc") dataMap.put("field5", null) val bodyData = requestBodyData(USERNAME, USERID, ACCESS, KEY, dataMap) bodyData?.let { Log.d(TAG, "click :" + it) receiveInfo(POSTURL, it) } } else -> { Log.d(TAG, "設定がありません") } } } }
全体
このようなコードでエミュレートを実行した結果が概要のような結果になりました。
package org.sibainu.relax.room.jsonuploadkotlin import android.os.Bundle import android.util.Log import android.view.View import android.widget.Button import android.widget.TextView import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.appcompat.app.AppCompatActivity import org.json.JSONObject import org.sibainu.relax.room.jsonuploadkotlin.databinding.ActivityMainBinding import java.io.BufferedReader import java.io.InputStream import java.io.InputStreamReader import java.net.HttpURLConnection import java.net.SocketTimeoutException import java.net.URL import java.nio.charset.StandardCharsets import java.util.concurrent.Callable import java.util.concurrent.Executors class MainActivity : AppCompatActivity() { private lateinit var viewBinding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewBinding = ActivityMainBinding.inflate(layoutInflater) setContentView(viewBinding.root) findViewById<Button>(R.id.upload_button).setOnClickListener(ClickListener()) } @UiThread private fun receiveInfo(postUrl: String, bodyData: String) { val backgroundReceiver = InfoBackground(postUrl, bodyData) val executeService = Executors.newSingleThreadExecutor() Log.d(TAG, "receiveInfo executeService :" + bodyData) val future = executeService.submit(backgroundReceiver) Log.d(TAG, "receiveInfo future :" + bodyData) val result = future.get() if (result != "") { showInfo(result) } else { showInfo("{\"username\":\"失敗しました\"}") } } @UiThread private fun showInfo(result: String) { Log.d(TAG, "showInfo :" + result) //レスポンスJSONオブジェクトを生成。 val rootJSON = JSONObject(result) rootJSON?.apply { val userName = rootJSON.getString("username") Log.d(TAG, "ユーザー名 :" + userName) val telop = "ユーザー名:${userName}" val tvTelop = findViewById<TextView>(R.id.textView2) tvTelop.text = telop } } private fun requestBodyData(username: String, userid: String, access: String, key: String, jsondata: Map<Any?,Any?>): String { val postmap: MutableMap<Any?, Any?> = mutableMapOf("username" to username) postmap.put("userid", userid) postmap.put("access", access) postmap.put("key", key) postmap.put("data", jsondata) return JSONObject(postmap).toString() } private inner class InfoBackground(postUrl: String, bodyData: String): Callable<String> { val _postUrl = postUrl val _bodyData = bodyData @WorkerThread override fun call(): String { // POSTデータ。 val postData = _bodyData // 送信結果が格納される。 var result = "" // URLオブジェクトを生成。 val url = URL(_postUrl) // URLオブジェクトからHttpURLConnectionオブジェクトを取得。 val con = url.openConnection() as HttpURLConnection // 接続に使ってもよい時間を設定。 con.connectTimeout = 1000 // データ取得に使ってもよい時間。 con.readTimeout = 1000 // HTTP接続メソッドをPOSTに設定。 con.requestMethod = "POST" con.doOutput = true con.setRequestProperty("Content-type", "application/json; charset=utf-8") con.useCaches = false Log.d(TAG, "InfoBackground" + _bodyData) try { // 接続。 con.connect() Log.d(TAG, "connect :" + _bodyData) // アップロード val os = con.outputStream os?.run { // 送信するデータをバイト配列に変換 val postDataBytes = postData.toByteArray(charset("UTF-8")) // アップロードの実行 write(postDataBytes) // OnputStreamオブジェクトを解放。 flush() close() val statusCode = con.responseCode Log.d(TAG, "statuscode :" + statusCode.toString()) if (statusCode == HttpURLConnection.HTTP_OK) { // レスポンスデータであるInputStreamオブジェクトを文字列に変換。 val stream = con.inputStream result = is2String(stream) } } Log.d(TAG, "inputstream :" + _bodyData) } catch(ex: SocketTimeoutException) { Log.w(TAG, "通信タイムアウト", ex) } finally { con.disconnect() } Log.d(TAG, "result :" + result) return result } private fun is2String(stream: InputStream): String { val sb = StringBuilder() val reader = BufferedReader(InputStreamReader(stream, StandardCharsets.UTF_8)) var line = reader.readLine() while(line != null) { sb.append(line) line = reader.readLine() } reader.close() return sb.toString() } } companion object { private const val TAG = "AsyncSample" private const val POSTURL = "https://サーバー/script.php" private const val USERNAME = "name" private const val USERID = "userid" private const val ACCESS = "access" private const val KEY = "key" } private inner class ClickListener: View.OnClickListener { override fun onClick(v: View?) { when (v?.id) { R.id.upload_button -> { val dataMap: MutableMap<Any?, Any?> = mutableMapOf("field1" to "hoge hoge") dataMap.put("field2", "1234567890") dataMap.put("field3", "root") dataMap.put("field4", "abc") dataMap.put("field5", null) val bodyData = requestBodyData(USERNAME, USERID, ACCESS, KEY, dataMap) bodyData?.let { Log.d(TAG, "click :" + it) receiveInfo(POSTURL, it) } } else -> { Log.d(TAG, "設定がありません") } } } } }
最後にスマホでの実行状況を載せます。
左が起動時で、右がボタンをクリックした結果です。エミュレートのそのままです。
今回はここまでとします。