From 0f4d3d871df247402a2e74e7ac182586a32095d2 Mon Sep 17 00:00:00 2001 From: Cateyes Date: Wed, 7 Oct 2020 11:01:39 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E5=8A=A0=E5=85=A5=20Retrofit=20=E7=9B=B8?= =?UTF-8?q?=E9=97=9C=20Library?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index f12fb9a..60c643d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,4 +64,11 @@ dependencies { // optional - Test helpers testImplementation "androidx.room:room-testing:$room_version" /*** Room Dependencies End ***/ + + /*** Retrofit Dependencies Start ***/ + // 會連帶引用 OkHttp,因此不必再 implementation + // 這裡使用最新版,若要支援舊版 Android 請注意使用的版本。 + // 詳見:https://github.com/square/retrofit/blob/master/CHANGELOG.md + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + /*** Retrofit Dependencies End ***/ } \ No newline at end of file From 711fbccd3eafd4c118e6a390c80db668e98693a9 Mon Sep 17 00:00:00 2001 From: Cateyes Date: Wed, 7 Oct 2020 18:20:26 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E5=8A=A0=E4=B8=8A=E8=A6=81=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=B6=B2=E8=B7=AF=E6=99=82=E9=9C=80=E8=A6=81=E7=9A=84?= =?UTF-8?q?=E6=AC=8A=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0dec640..7c687f4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + Date: Wed, 7 Oct 2020 18:26:52 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E5=8A=A0=E4=B8=8A=E7=B6=B2=E8=B7=AF?= =?UTF-8?q?=E5=AD=98=E5=8F=96=E7=9A=84=E5=9F=BA=E6=9C=AC=E7=AF=84=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 5 ++ .../kotlinandroidworkshop/TodoListFragment.kt | 18 +++++ .../network/ApiClient.kt | 69 +++++++++++++++++++ .../kotlinandroidworkshop/network/UserApi.kt | 24 +++++++ .../network/entity/Envelope.kt | 5 ++ .../network/entity/User.kt | 11 +++ .../repository/UserRepository.kt | 15 ++++ .../network/ApiClientTest.kt | 62 +++++++++++++++++ 8 files changed, 209 insertions(+) create mode 100644 app/src/main/java/tw/andyang/kotlinandroidworkshop/network/ApiClient.kt create mode 100644 app/src/main/java/tw/andyang/kotlinandroidworkshop/network/UserApi.kt create mode 100644 app/src/main/java/tw/andyang/kotlinandroidworkshop/network/entity/Envelope.kt create mode 100644 app/src/main/java/tw/andyang/kotlinandroidworkshop/network/entity/User.kt create mode 100644 app/src/main/java/tw/andyang/kotlinandroidworkshop/repository/UserRepository.kt create mode 100644 app/src/test/java/tw/andyang/kotlinandroidworkshop/network/ApiClientTest.kt diff --git a/app/build.gradle b/app/build.gradle index 60c643d..fe54651 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -70,5 +70,10 @@ dependencies { // 這裡使用最新版,若要支援舊版 Android 請注意使用的版本。 // 詳見:https://github.com/square/retrofit/blob/master/CHANGELOG.md implementation 'com.squareup.retrofit2:retrofit:2.9.0' + // 因為我們 API 格式為 JSON,因此使用 GSON 來幫忙轉為物件 + implementation "com.squareup.retrofit2:converter-gson:2.9.0" /*** Retrofit Dependencies End ***/ + + implementation "com.squareup.okhttp3:logging-interceptor:3.14.9" + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9' } \ No newline at end of file diff --git a/app/src/main/java/tw/andyang/kotlinandroidworkshop/TodoListFragment.kt b/app/src/main/java/tw/andyang/kotlinandroidworkshop/TodoListFragment.kt index a6a429d..7e9e50e 100644 --- a/app/src/main/java/tw/andyang/kotlinandroidworkshop/TodoListFragment.kt +++ b/app/src/main/java/tw/andyang/kotlinandroidworkshop/TodoListFragment.kt @@ -1,18 +1,25 @@ package tw.andyang.kotlinandroidworkshop import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager +import com.google.gson.Gson import kotlinx.android.synthetic.main.fragment_todo_list.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import tw.andyang.kotlinandroidworkshop.database.AppDatabase import tw.andyang.kotlinandroidworkshop.mvvm.AnyViewModelFactory +import tw.andyang.kotlinandroidworkshop.network.ApiClient import tw.andyang.kotlinandroidworkshop.repository.TodoItemRepository class TodoListFragment : Fragment() { @@ -59,5 +66,16 @@ class TodoListFragment : Fragment() { buttonAdd.setOnClickListener { findNavController().navigate(TodoListFragmentDirections.actionMainFragmentToAddTodoFragment()) } + + + GlobalScope.launch(Dispatchers.Main) { + val gson = Gson() + val client = ApiClient() + Log.d("Hello", gson.toJson(client.listUser())) + val userNames = client.listUser().map { it.lastName } + activity?.run { + Toast.makeText(this, userNames.joinToString(", "), Toast.LENGTH_LONG).show() + } + } } } \ No newline at end of file diff --git a/app/src/main/java/tw/andyang/kotlinandroidworkshop/network/ApiClient.kt b/app/src/main/java/tw/andyang/kotlinandroidworkshop/network/ApiClient.kt new file mode 100644 index 0000000..369dc68 --- /dev/null +++ b/app/src/main/java/tw/andyang/kotlinandroidworkshop/network/ApiClient.kt @@ -0,0 +1,69 @@ +package tw.andyang.kotlinandroidworkshop.network + +import okhttp3.Call +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import tw.andyang.kotlinandroidworkshop.network.entity.User + +class ApiClient { + + private val okHttp: Call.Factory + private val retrofit: Retrofit + private val userApi: UserApi + + + constructor(baseUrl: String = "https://reqres.in/api/") { + this.okHttp = OkHttpClient.Builder() + .addInterceptor(HttpLoggingInterceptor().apply { setLevel(HttpLoggingInterceptor.Level.BODY); }) + .build() + this.retrofit = Retrofit.Builder() + // 注意!base url 要斜線結尾! + .baseUrl(baseUrl) + .client(this.okHttp) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + this.userApi = retrofit.create(UserApi::class.java) + } + + suspend fun listUser(): List { + try { + val response = userApi.listUser() + return if (response.isSuccessful) { + response.body()!!.data + } else { + throw Exception("should success") + } + } catch (e: Exception) { + throw e + } + } + + suspend fun getUser(userId: Int): User { + try { + val response = userApi.getUser(userId) + return if (response.isSuccessful) { + response.body()!!.data + } else { + throw Exception("should success") + } + } catch (e: Exception) { + throw e + } + } + + suspend fun createUser(userName: String, job: String): User { + try { + val response = userApi.createUser(userName, job) + return if (response.isSuccessful) { + response.body()!! + } else { + throw Exception("should success") + } + } catch (e: Exception) { + throw e + } + } +} \ No newline at end of file diff --git a/app/src/main/java/tw/andyang/kotlinandroidworkshop/network/UserApi.kt b/app/src/main/java/tw/andyang/kotlinandroidworkshop/network/UserApi.kt new file mode 100644 index 0000000..6f67142 --- /dev/null +++ b/app/src/main/java/tw/andyang/kotlinandroidworkshop/network/UserApi.kt @@ -0,0 +1,24 @@ +package tw.andyang.kotlinandroidworkshop.network + +import retrofit2.Response +import retrofit2.http.* +import tw.andyang.kotlinandroidworkshop.network.entity.Envelope +import tw.andyang.kotlinandroidworkshop.network.entity.User + +interface UserApi { + + companion object { + const val PATH_ARG_USER_ID = "user_id" + } + + // 注意!不要斜線開頭! + @GET("users") + suspend fun listUser(): Response>> + + @GET("users/{$PATH_ARG_USER_ID}") + suspend fun getUser(@Path(PATH_ARG_USER_ID) userId: Int): Response> + + @FormUrlEncoded + @POST("users") + suspend fun createUser(@Field("name") userName: String, @Field("job") job: String): Response +} \ No newline at end of file diff --git a/app/src/main/java/tw/andyang/kotlinandroidworkshop/network/entity/Envelope.kt b/app/src/main/java/tw/andyang/kotlinandroidworkshop/network/entity/Envelope.kt new file mode 100644 index 0000000..62eadfb --- /dev/null +++ b/app/src/main/java/tw/andyang/kotlinandroidworkshop/network/entity/Envelope.kt @@ -0,0 +1,5 @@ +package tw.andyang.kotlinandroidworkshop.network.entity + +data class Envelope( + val data: T +) \ No newline at end of file diff --git a/app/src/main/java/tw/andyang/kotlinandroidworkshop/network/entity/User.kt b/app/src/main/java/tw/andyang/kotlinandroidworkshop/network/entity/User.kt new file mode 100644 index 0000000..06580ff --- /dev/null +++ b/app/src/main/java/tw/andyang/kotlinandroidworkshop/network/entity/User.kt @@ -0,0 +1,11 @@ +package tw.andyang.kotlinandroidworkshop.network.entity + +import com.google.gson.annotations.SerializedName + +class User( + @SerializedName("id") val id: Int, + @SerializedName("email") var email: String, + @SerializedName("first_name") var firstName: String, + @SerializedName("last_name") var lastName: String, + @SerializedName("avatar") var avatar: String +) \ No newline at end of file diff --git a/app/src/main/java/tw/andyang/kotlinandroidworkshop/repository/UserRepository.kt b/app/src/main/java/tw/andyang/kotlinandroidworkshop/repository/UserRepository.kt new file mode 100644 index 0000000..c785058 --- /dev/null +++ b/app/src/main/java/tw/andyang/kotlinandroidworkshop/repository/UserRepository.kt @@ -0,0 +1,15 @@ +package tw.andyang.kotlinandroidworkshop.repository + +import tw.andyang.kotlinandroidworkshop.network.ApiClient +import tw.andyang.kotlinandroidworkshop.network.entity.User + +class UserRepository(private val apiClient: ApiClient) { + + suspend fun listUser(): List { + return apiClient.listUser() + } + + suspend fun getUser(userId: Int): User { + return apiClient.getUser(userId) + } +} \ No newline at end of file diff --git a/app/src/test/java/tw/andyang/kotlinandroidworkshop/network/ApiClientTest.kt b/app/src/test/java/tw/andyang/kotlinandroidworkshop/network/ApiClientTest.kt new file mode 100644 index 0000000..b8dec06 --- /dev/null +++ b/app/src/test/java/tw/andyang/kotlinandroidworkshop/network/ApiClientTest.kt @@ -0,0 +1,62 @@ +package tw.andyang.kotlinandroidworkshop.network + +import com.google.gson.Gson +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.newSingleThreadContext +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +class ApiClientTest { + + private val mainThreadSurrogate = newSingleThreadContext("UI thread") + + @Before + fun setUp() { + Dispatchers.setMain(mainThreadSurrogate) + } + + @After + fun tearDown() { + Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher + mainThreadSurrogate.close() + } + + @Test + fun testListUser() { + runBlocking { + launch(Dispatchers.Main) { + val gson = Gson() + val apiClient = ApiClient() + println(gson.toJson(apiClient.listUser())) + } + } + } + + @Test + fun testGetUser() { + runBlocking { + launch(Dispatchers.Main) { + val gson = Gson() + val apiClient = ApiClient() + println(gson.toJson(apiClient.getUser(1))) + } + } + } + + @Test + fun testCreateUser() { + runBlocking { + launch(Dispatchers.Main) { + val gson = Gson() + val apiClient = ApiClient() + println(gson.toJson(apiClient.createUser("morpheus", "leader"))) + } + } + } +} \ No newline at end of file