Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,16 @@ 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'
// 因為我們 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'
}
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="tw.andyang.kotlinandroidworkshop">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<User> {
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
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Envelope<List<User>>>

@GET("users/{$PATH_ARG_USER_ID}")
suspend fun getUser(@Path(PATH_ARG_USER_ID) userId: Int): Response<Envelope<User>>

@FormUrlEncoded
@POST("users")
suspend fun createUser(@Field("name") userName: String, @Field("job") job: String): Response<User>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package tw.andyang.kotlinandroidworkshop.network.entity

data class Envelope<T>(
val data: T
)
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
@@ -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<User> {
return apiClient.listUser()
}

suspend fun getUser(userId: Int): User {
return apiClient.getUser(userId)
}
}
Original file line number Diff line number Diff line change
@@ -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")))
}
}
}
}