## 名人語錄
點擊螢幕可以更換不同的名人語錄,不會點擊了之後還是相同的名人語錄
```kotlin=
package com.example.myfirstcomposeapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.myfirstcomposeapp.ui.theme.MyFirstComposeAppTheme
data class Quote(val message: String, val from: String)
fun getNextQuote(currentQuote: Quote, quotes: List<Quote>): Quote {
val otherQuotes = quotes.filter { it != currentQuote }
return otherQuotes.random()
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val quotes = listOf(
Quote("Life is like a box of chocolates.", "Forrest Gump"),
Quote("Imagination is more important than knowledge.", "Albert Einstein"),
Quote("Time you enjoy wasting, was not wasted.", "John Lennon"),
)
var currentQuote by remember { mutableStateOf(quotes.random()) }
MyFirstComposeAppTheme {
Surface(
modifier = Modifier
.fillMaxSize()
.clickable {
currentQuote = getNextQuote(currentQuote, quotes)
},
color = MaterialTheme.colorScheme.background
) {
QuoteImage(message = currentQuote.message, from = currentQuote.from)
}
}
}
}
}
@Composable
fun QuoteText(message: String, from: String, modifier: Modifier = Modifier) {
// Create a column so that texts don't overlap
Column(
verticalArrangement = Arrangement.Center,
modifier = modifier
) {
Text(
text = "\"" + message + "\"",
fontSize = 48.sp,
lineHeight = 55.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 16.dp),
)
Text(
text = from,
fontSize = 36.sp,
fontStyle = FontStyle.Italic,
modifier = Modifier
.padding(top = 16.dp)
.padding(end = 16.dp)
.align(alignment = Alignment.End)
)
}
}
@Composable
fun QuoteImage(message: String, from: String, modifier: Modifier = Modifier) {
Box(modifier) {
Image(
painter = painterResource(R.drawable.quote_background_1),
contentDescription = null,
contentScale = ContentScale.Crop,
alpha = 0.5f,
modifier = Modifier.fillMaxSize()
)
QuoteText(
message = message,
from = from,
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
)
}
}
@Preview(showBackground = true)
@Composable
fun QuotePreview() {
val quotes = listOf(
Quote("Life is like a box of chocolates.", "Forrest Gump"),
Quote("Imagination is more important than knowledge.", "Albert Einstein"),
Quote("Time you enjoy wasting, was not wasted.", "John Lennon"),
)
var currentQuote by remember { mutableStateOf(quotes.random()) }
MyFirstComposeAppTheme {
Surface(
modifier = Modifier
.fillMaxSize()
.clickable {
currentQuote = getNextQuote(currentQuote, quotes)
},
color = MaterialTheme.colorScheme.background
) {
QuoteImage(message = currentQuote.message, from = currentQuote.from)
}
}
}
```
# Quote API
Quotable API
* https://github.com/lukePeavey/quotable
* Documentation https://docs.quotable.io/docs/api/ZG9jOjQ2NDA2-introduction
## 從 API 讀取名言
```kotlin=
package com.example.myfirstcomposeapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.myfirstcomposeapp.ui.theme.MyFirstComposeAppTheme
import com.google.gson.Gson
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
data class Quote(val message: String, val from: String)
data class QuoteFromApi(
val _id: String,
val content: String,
val author: String,
val tags: List<String>,
val authorSlug: String,
val length: Int,
val dateAdded: String,
val dateModified: String
)
fun getQuoteFromApi(): QuoteFromApi? {
val url = URL("https://api.quotable.io/random")
with(url.openConnection() as HttpURLConnection) {
requestMethod = "GET"
println("Response Code: $responseCode")
BufferedReader(InputStreamReader(inputStream)).use {
val response = it.readText()
val gson = Gson()
return gson.fromJson(response, QuoteFromApi::class.java)
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val quotes = listOf(
Quote("Life is like a box of chocolates.", "Forrest Gump"),
Quote("Imagination is more important than knowledge.", "Albert Einstein"),
Quote("Time you enjoy wasting, was not wasted.", "John Lennon"),
)
var currentQuote by remember { mutableStateOf(quotes.random()) }
MyFirstComposeAppTheme {
Surface(
modifier = Modifier
.fillMaxSize()
.clickable {
currentQuote = quotes.random()
},
color = MaterialTheme.colorScheme.background
) {
QuoteImage(message = currentQuote.message, from = currentQuote.from)
}
}
}
}
}
@Composable
fun QuoteText(message: String, from: String, modifier: Modifier = Modifier) {
// Create a column so that texts don't overlap
Column(
verticalArrangement = Arrangement.Center,
modifier = modifier
) {
Text(
text = "\"" + message + "\"",
fontSize = 48.sp,
lineHeight = 55.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 16.dp),
)
Text(
text = from,
fontSize = 36.sp,
fontStyle = FontStyle.Italic,
modifier = Modifier
.padding(top = 16.dp)
.padding(end = 16.dp)
.align(alignment = Alignment.End)
)
}
}
@Composable
fun QuoteImage(message: String, from: String, modifier: Modifier = Modifier) {
Box(modifier) {
Image(
painter = painterResource(R.drawable.quote_background_1),
contentDescription = null,
contentScale = ContentScale.Crop,
alpha = 0.5f,
modifier = Modifier.fillMaxSize()
)
QuoteText(
message = message,
from = from,
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
)
}
}
@Preview(showBackground = true)
@Composable
fun QuotePreview() {
val quotes = listOf(
Quote("Life is like a box of chocolates.", "Forrest Gump"),
Quote("Imagination is more important than knowledge.", "Albert Einstein"),
Quote("Time you enjoy wasting, was not wasted.", "John Lennon"),
)
var currentQuote by remember { mutableStateOf(quotes.random()) }
MyFirstComposeAppTheme {
Surface(
modifier = Modifier
.fillMaxSize()
.clickable {
//currentQuote = quotes.random()
var quoteFromApi = getQuoteFromApi()
if (quoteFromApi != null) {
currentQuote = Quote(quoteFromApi.content, quoteFromApi.author)
}
},
color = MaterialTheme.colorScheme.background
) {
QuoteImage(message = currentQuote.message, from = currentQuote.from)
}
}
}
```
Note:
* 需要在 build.gradle.kts 裡加入
`implementation("com.google.code.gson:gson:2.10.1")`
* 方便看 JSON 格式的線上工具
* https://jsonformatter.curiousconcept.com/
* 在模擬器與實機上需要 INTERNET 的權限
`<uses-permission android:name="android.permission.INTERNET" />`
有時可能需要將 app 移除之後再安裝才可正常取得權限
取得 thread 限制
* macOS
`sysctl kern.num_threads`
* Linux
`cat /proc/sys/kernel/threads-max`
## 五秒內要出拳的剪刀石頭布遊戲
```kotlin=
package com.example.hellokotlin
import java.util.Scanner
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
enum class Choice {
ROCK, PAPER, SCISSORS
}
fun getPlayerInput(): String? {
val executor = Executors.newSingleThreadExecutor()
val scanner = Scanner(System.`in`)
val future: Future<String> = executor.submit<String> { scanner.nextLine() }
return try {
future.get(5, TimeUnit.SECONDS)
} catch (e: TimeoutException) {
null
} finally {
future.cancel(true)
executor.shutdown()
}
}
fun main() {
var playerWins = 0
var computerWins = 0
var round = 0
while (playerWins < 2 && computerWins < 2) {
round++
println("現在是第 $round 局")
var result = 0
do {
println("請五秒內出拳:Rock, Paper, Scissors")
val playerInput = getPlayerInput()?.trim()?.lowercase()
if (playerInput == null) {
println("超過時間,你輸了這局\n")
computerWins++
break
}
val playerChoice = when (playerInput) {
"rock", "r" -> Choice.ROCK
"paper", "p" -> Choice.PAPER
"scissors", "s" -> Choice.SCISSORS
else -> {
println("輸入錯誤,請重新輸入\n")
continue
}
}
val computerChoice = Choice.entries.random()
result = (playerChoice.ordinal - computerChoice.ordinal + 3) % 3
println("玩家出拳:$playerChoice")
println("電腦出拳:$computerChoice")
println(
"結果:${
when (result) {
0 -> "平手"
1 -> {
playerWins++
"你贏了"
}
else -> {
computerWins++
"你輸了"
}
}
}\n"
)
} while (result == 0)
}
if (playerWins > computerWins) {
println("恭喜你獲得最終勝利!")
} else {
println("很遺憾,最終是電腦獲勝了。")
}
}
```
用 Kotlin coroutines 的方式來寫
```kotlin=
package com.example.hellokotlin
import android.app.admin.SystemUpdatePolicy
import kotlinx.coroutines.*
import java.util.Scanner
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
enum class Choice {
ROCK, PAPER, SCISSORS
}
suspend fun getPlayerInput(): String? = withTimeoutOrNull(5000L) {
var scanner = Scanner(System.`in`)
while (true) {
delay(100)
if (System.`in`.available() > 0) {
return@withTimeoutOrNull scanner.nextLine()
}
}
null
}
fun main() = runBlocking {
var playerWins = 0
var computerWins = 0
var round = 0
while (playerWins < 2 && computerWins < 2) {
round++
println("現在是第 $round 局")
var result = 0
do {
println("請五秒內出拳:Rock, Paper, Scissors")
val playerInput = getPlayerInput()?.trim()?.lowercase()
if (playerInput == null) {
println("超過時間,你輸了這局\n")
computerWins++
break
}
val playerChoice = when (playerInput) {
"rock", "r" -> Choice.ROCK
"paper", "p" -> Choice.PAPER
"scissors", "s" -> Choice.SCISSORS
else -> {
println("輸入錯誤,請重新輸入\n")
continue
}
}
val computerChoice = Choice.entries.random()
result = (playerChoice.ordinal - computerChoice.ordinal + 3) % 3
println("玩家出拳:$playerChoice")
println("電腦出拳:$computerChoice")
println(
"結果:${
when (result) {
0 -> "平手"
1 -> {
playerWins++
"你贏了"
}
else -> {
computerWins++
"你輸了"
}
}
}\n"
)
} while (result == 0)
}
if (playerWins > computerWins) {
println("恭喜你獲得最終勝利!")
} else {
println("很遺憾,最終是電腦獲勝了。")
}
}
```
## 使用 Retrofit 改寫名人語錄
加入到 `build.gradle.kts`
`implementation("com.squareup.retrofit2:retrofit:2.11.0")`
`implementation("com.squareup.retrofit2:converter-gson:2.11.0")`
```kotlin=
package com.example.myfirstcomposeapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.lifecycleScope
import com.example.myfirstcomposeapp.ui.theme.MyFirstComposeAppTheme
import com.google.gson.Gson
import kotlinx.coroutines.launch
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
interface QuotableApi {
@GET("random")
suspend fun getRandomQuote(): QuoteFromApi
}
data class Quote(val message: String, val from: String)
data class QuoteFromApi(
val _id: String,
val content: String,
val author: String,
val tags: List<String>,
val authorSlug: String,
val length: Int,
val dateAdded: String,
val dateModified: String
)
fun getQuoteFromApi(): QuoteFromApi? {
val url = URL("https://api.quotable.io/random")
with(url.openConnection() as HttpURLConnection) {
requestMethod = "GET" // optional default is GET
println("\nSending 'GET' request to URL : $url")
println("Response Code : $responseCode")
BufferedReader(InputStreamReader(inputStream)).use {
val response = it.readText() //This is your response
val gson = Gson()
val quote = gson.fromJson(response, QuoteFromApi::class.java)
return quote
}
}
}
class MainActivity : ComponentActivity() {
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://api.quotable.io/")
.addConverterFactory(GsonConverterFactory.create())
.build()
private val api = retrofit.create(QuotableApi::class.java)
private suspend fun getRandomQuote(): QuoteFromApi? {
return api.getRandomQuote()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create a list of quotes
val quotes = listOf(
Quote("Life is like a box of chocolates.", "Forrest Gump"),
Quote("Imagination is more important than knowledge.", "Albert Einstein"),
Quote("Time you enjoy wasting, was not wasted.", "John Lennon"),
)
setContent {
// Create a state for the current quote
var currentQuote by remember { mutableStateOf(quotes.random()) }
MyFirstComposeAppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier
.fillMaxSize()
.clickable { // Add the clickable modifier
// Update the current quote state with a new random quote when the surface is clicked
lifecycleScope.launch {
val quoteFromApi = getRandomQuote()
if (quoteFromApi != null) {
currentQuote = Quote(quoteFromApi.content, quoteFromApi.author)
}
}
},
color = MaterialTheme.colorScheme.background
) {
QuoteImage(
message = currentQuote.message,
from = currentQuote.from,
)
}
}
}
}
}
@Composable
fun QuoteText(message: String, from: String, modifier: Modifier = Modifier) {
// Create a column so that texts don't overlap
Column(
verticalArrangement = Arrangement.Center,
modifier = modifier
) {
Text(
text = "\"" + message + "\"",
fontSize = 48.sp,
lineHeight = 55.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 16.dp),
)
Text(
text = from,
fontSize = 36.sp,
// Make the font italic
fontStyle = FontStyle.Italic,
modifier = Modifier
.padding(top = 16.dp)
.padding(end = 16.dp)
.align(alignment = Alignment.End)
)
}
}
@Composable
fun QuoteImage(message: String, from: String, modifier: Modifier = Modifier) {
Box(modifier) {
Image(
painter = painterResource(R.drawable.quote_background_1),
contentDescription = null,
contentScale = ContentScale.Crop,
alpha = 0.5f,
modifier = Modifier.fillMaxSize()
)
QuoteText(
message = message,
from = from,
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
)
}
}
@Preview(showBackground = true)
@Composable
fun QuotePreview() {
// Create a list of quotes
val quotes = listOf(
Quote("Life is like a box of chocolates.", "Forrest Gump"),
Quote("Imagination is more important than knowledge.", "Albert Einstein"),
Quote("Time you enjoy wasting, was not wasted.", "John Lennon"),
)
// Create a state for the current quote
var currentQuote by remember { mutableStateOf(quotes.random()) }
MyFirstComposeAppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier
.fillMaxSize()
.clickable { // Add the clickable modifier
// Update the current quote state with a new random quote when the surface is clicked
//currentQuote = quotes.random()
val quoteFromApi = getQuoteFromApi()
if (quoteFromApi != null) {
currentQuote = Quote(quoteFromApi.content, quoteFromApi.author)
}
},
color = MaterialTheme.colorScheme.background
) {
QuoteImage(
message = currentQuote.message,
from = currentQuote.from,
)
}
}
}
```