Androidからサーバ側にJSONでデータの送受信をする必要があり、RetrofitでJSON over HTTPの実装をしてみました。
build.gradle
RetrofitはOkHttpのラッパーなので内部でOkHttpを使っています。
JSONで送受信するためには、JSONのパーサ、複数選択可能ですが、今回はmoshiを使いました。
Timberはロガーです。(任意)
dependencies {
(中略)
// Retrofit
def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11'
implementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
implementation 'com.jakewharton.timber:timber:4.7.1'
}
AndroidManifest.xml
HTTPで通信するためにはManifestにpermissionの設定が必要です。
更に、HTTPSではなく、素のHTTPで通信する場合はandroid:usesCleartextTrafficの設定も必要です。
<?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:usesCleartextTraffic="true">
<activity
(中略)
</activity>
</application>
</manifest>
リクエストとレスポンスのクラスを作る
JSONのリクエスト、レスポンスに相当するクラスを作ります。ここでは簡単なユーザ名、パスワードでログインする例を作ってみます。
data class LoginRequest(
/** ログインID */
@Json(name = "loginId")
var loginId: String,
/** パスワード */
@Json(name = "password")
var password: String
)
@Json(name = "・・・")のアノテーションはkotlinのフィールド名がJSONのフィールド名と一致しているなら省略できます。例として書いておきました。
data class LoginResponse(
/** ログインID */
@Json(name = "loginId")
var loginId: String,
/** エラーメッセージ */
@Json(name = "message")
var message: String
)
ログインIDは冗長かもしれませんが・・・ログインに成功した場合はリクエストのログインIDをそのままログインIDに設定し、メッセージは空。失敗した場合はログインIDは空、メッセージにその理由が入っている想定です。
リクエストとレスピンスのJSONは以下のようなイメージになります。
{
"loginId":"123456",
"password":"testpass"
}
{
"loginId": "123456",
"message": ""
}
{
"loginId": "",
"message": "パスワード誤り"
}
リクエストとレスポンスのクラスはdataクラスをネストすればそれに応じてJSONもネストします、Listを使えば、JSONは繰り返しになります。
Serviceインタフェースを作る
HTTPで送受信するためのServiceインタフェースを作ります。
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST
interface SampleService {
/**
* ログイン
*/
@POST("sampleServiee/api/login")
suspend fun doLogin(
@Body request: LoginRequest
): Response<LoginResponse>
}
インタフェースは1関数が1エンドポイントになります。
今回はリクエストもレスポンスもJSONなのでHTTP POSTメソッドを使います、@POSTアノテーションの引数にURLのパス部分だけ指定します。(ここは後述でURL全体を指定するように改善)
リクエストは関数の引数で@Bodyアノテーションを付けます。
レスポンスは関数の戻り値でretrofit2.Responseでラップします。
JSON over HTTPは非同期で(AndroidはI/Oに関するものは非同期)coRoutineを使うのでsupend関数になっています。
Serviceを初期化するRepositoryクラスを作る
class SampleRepository(baseUrl: String, timeout: Long) {
/** サービス */
private var service: SampleService
init {
// ロギング
val logging = HttpLoggingInterceptor {
Timber.tag("OkHttp").d(it)
}
logging.setLevel(HttpLoggingInterceptor.Level.BASIC)
val client = OkHttpClient.Builder()
.readTimeout(timeout, TimeUnit.SECONDS)
.connectTimeout(timeout, TimeUnit.SECONDS)
.addInterceptor(logging)
.build()
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
service = Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
.create(SampleService::class.java)
}
/**
* ログイン
*/
suspend fun doLogin(request: LoginRequest): Response<LoginResponse> = service.doLogin(request)
}
流れは見てもらえばわかる通りです。ロギングを有効にして(任意)、OkHttpのクライアントを初期化、moshi(JSONパーサ)を初期化して、Retrofit.BuilderにmoshiとServiceインタフェースを渡す。
あとはServiceインタフェースを通してやりとり。ここでもcoRoutineで非同期として呼び出すので、ログイン関数はsupend関数になっています。
このRepositoryクラスとServiceインタフェースを見ればわかる通り、URLのホスト部分とパス部分がRepositoryクラスとServiceインタフェースで別れてしまっています。(後述で改善)
viewModelの中から呼び出す。
あとはviewModelの中から呼び出します。
class SampleViewModel(
private val sampleRepository: SampleRepository): ViewModel() {
fun doLogin(userId: String, password: String) {
val request = LoginRequest(userId, password)
viewModelScope.launch(Dispatchers.IO) {
try {
val response = sampleRepository.doLogin(request)
if (response.isSuccessful) {
if (response.body()?.message?.isNotBlank() == true) {
// ログイン失敗
} else {
// ログイン成功
}
} else {
// 送信失敗
}
} catch (e: Exception) {
// 通信できない
}
}
}
}
retorofitを呼び出して、response.isSuccessfulの場合はサーバ側の呼び出しは成功しています。あとはログインが成功か、失敗か。
response.isSuccessfulがfalseの場合は呼び出しが失敗。(404 not foundとか、何らかのHTTPレスポンスが返ってくる)
そもそも通信が確立できなかった場合(connetion timeout等)の場合はcatch節に入ります。
あとはMainActivity、FragmentからこのviewModelを初期化して関数を呼び出せばいいです。
改善、URLを可変にしたい
例えば、送信するURLをsharedPreferenceに持ちたい場合、条件によってURLを変えたい場合等はURLは都度可変になります。
上のserviceインタフェースでは、
@POST("sampleServiee/api/login")
アノテーションの引数にパスを書いてしまっているのでこれでは固定になってしまいます。
これを可変にするためには、Retrofitの@Urlアノテーションを使います。
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.Url
interface SampleService {
/**
* ログイン
*/
@POST
suspend fun doLogin(
@Url url: String,
@Body request: LoginRequest
): Response<LoginResponse>
}
@Postアノテーションの引数は省略して、代わりに@Urlアノテーションを付けた引数を追加してやります。
これによって、呼び出す都度、URLは可変にすることができます。
Repository、ViewModelも引数を増やしてやります。
class SampleRepository(baseUrl: String, timeout: Long) {
(中略)
/**
* ログイン
*/
suspend fun doLogin(url: String, request: LoginRequest): Response<LoginResponse> = service.doLogin(url, request)
}
class SampleViewModel(
private val sampleRepository: SampleRepository): ViewModel() {
fun doLogin(url: String, userId: String, password: String) {
val request = LoginRequest(userId, password)
viewModelScope.launch(Dispatchers.IO) {
try {
val response = sampleRepository.doLogin(url, request)
if (response.isSuccessful) {
if (response.body()?.message?.isNotBlank() == true) {
// ログイン失敗
} else {
// ログイン成功
}
} else {
// 送信失敗
}
} catch (e: Exception) {
// 通信できない
}
}
}
}
@UrlアノテーションによるURLの可変はRetrofit2からの機能です。この辺のことが公式ホームページははっきりと書かれていません。
最後に
RetrofitはAndroidのHTTP通信ライブラリとして人気が高いです。(と、chatGPTが言っていた)
他にも色々な使い方があります。
他、REST的な使い方、
@GET("group/{id}/users")
とか、Multipartとかもできるようです。
今回はまずは簡単な入り口から、JSON over HTTPの実装の仕方について紹介しました。