## 名人語錄 點擊螢幕可以更換不同的名人語錄,不會點擊了之後還是相同的名人語錄 ```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, ) } } } ```