├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── ascii │ │ └── warmpackage │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── ascii │ │ │ └── warmpackage │ │ │ ├── Base64.java │ │ │ ├── MainActivity.kt │ │ │ ├── WarmApp.kt │ │ │ ├── WarmPackageView.kt │ │ │ ├── api │ │ │ ├── API.kt │ │ │ ├── APIBase.kt │ │ │ ├── InvoiceAPI.kt │ │ │ └── InvoiceResult.kt │ │ │ ├── model │ │ │ ├── WarmPackageModel.kt │ │ │ └── WarmService.kt │ │ │ └── presenter │ │ │ ├── MainPresenter.kt │ │ │ └── WarmPackagePresenter.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── content_main.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── ascii │ └── warmpackage │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | .DS_Store 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # IntelliJ 37 | *.iml 38 | .idea/* 39 | .idea/workspace.xml 40 | .idea/tasks.xml 41 | .idea/gradle.xml 42 | .idea/assetWizardSettings.xml 43 | .idea/dictionaries 44 | .idea/libraries 45 | .idea/caches 46 | 47 | # Keystore files 48 | # Uncomment the following line if you do not want to check your keystore files in. 49 | #*.jks 50 | 51 | # External native build folder generated in Android Studio 2.2 and later 52 | .externalNativeBuild 53 | 54 | # Google Services (e.g. APIs or Firebase) 55 | google-services.json 56 | 57 | # Freeline 58 | freeline.py 59 | freeline/ 60 | freeline_project_description.json 61 | 62 | # fastlane 63 | fastlane/report.xml 64 | fastlane/Preview.html 65 | fastlane/screenshots 66 | fastlane/test_output 67 | fastlane/readme.md 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ascii 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Warm Package 手機暖暖包 2 | 這是一個使用 Kotlin 語言撰寫的 Android 專案,此 App 的功能是在畫面上顯示現在的電池溫度,你可以使用 Seekbar 調整想要讓手機發燙到幾度,按了 Float Action Button 後 App 就會開始做事情讓手機發燙,發燙期間你可以離開應用程式,它會在 Service 繼續發燙,你也可以隨時透過 Notification 的資訊得知目前發燙的進度和點擊進入 App 停止程序。 3 | 4 | ## 這個專案示範了哪些技能 5 | 6 | - Kotlin 常用語法示範 7 | - Android Application 操作 8 | - Android Service 操作 9 | - Android Notification 操作 10 | - MVP 架構 11 | - Generic 泛型操作 12 | - Kotlin 操作 Java 現有 Library 的方式 (GSON、Volley 等) 13 | - 使用 Kotlin 開發專案時的 Unit Test 等測試工作 (待補) 14 | 15 | ## JVM、Java、Kotlin 關係簡介 16 | 首先透過一個簡單的表格了解 Kotlin 對於 Java Virtual Machine 與 Android 虛擬機 ( Dalvik / ART ) 的關係,並了解純 Kotlin 語言寫的程式碼能夠做到哪些事、何時該使用 -include-runtime 參數 。 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
LanguageJavaKotlin
Compilerjavackotlinc
Compile commendjavac text.javakotlinc text.kt -include-runtime -d test.jar
Execution fileByte Code ( .class / .jar / .dex )
Runtime EnvironmentJVM / Dalvik / ART
Native LibraryWindows / Mac OS / Linux / Solaris / Android
38 | 39 | ## Kotlin 相對於 Java 有什麼優點 40 | 41 | - 可以直接市面上使用 Java 撰寫的 Library,轉換成本僅是語言層面,不牽扯到框架層面 42 | - 支援 Functional programming 與 Closure、Property 等概念,相較於 Java 更先進靈巧 43 | - Java 僅能被編譯成 Byte Code 執行在 JVM 上,而 Kotlin 除了可以編譯為 Byte Code 執行在任何 JVM 環境上,例如 Spring、Android 之外,還可以被編譯為 JavaScript 產生 .js 檔案。 44 | - 理論上學一套 Kotlin 就可以吃遍全端 45 | 46 | ## 如何開始使用 Kotlin 47 | 48 | 因為 Kotlin 的函式庫很精簡,撰寫 App 時還是會操作到 Android SDK 與 JDK 中的類別或函式,加上 Kotlin 是編譯出運行在 JVM 之上的執行檔,所以還是要在對應平台上安裝 JDK 與 Java Runtime 後再安裝 Kotlin 編譯器。 49 | 50 | 如果你只是要拿來開發 Android App 且不打算在其他環境例如 Commend line 中執行,那使用安裝 Android Studio 3.0 以上版本自帶的 Kotlin 編譯器就很足夠了。如果想簡單試驗一些 Kotlin 語法的執行結果,可以在 Kotlin 官方的線上執行環境測試: [Try Kotlin](https://try.kotlinlang.org/) 51 | 52 | 在原有的 Android 專案中,僅需在 build.gradle 加入 plugin 即可 53 | 54 | - +apply plugin: 'kotlin-android' 55 | - +apply plugin: 'kotlin-android-extensions' 56 | - implementation 'org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.51' 57 | - compile org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.1.51 58 | 59 | 如果在網頁上 Copy 一段 Java 程式碼,貼到 Android Studio 的 .kt 檔案時 IDE 會詢問是否自動翻譯為 Kotlin,這是在操作較不熟悉的 Java base framework 時蠻好用的功能。接著,你要開始習慣指令尾端不再打上分號。 60 | 61 | ## Kotlin 的 Coding Conventions 62 | 63 | Kotlin 沒有 Java 的 int、boolean、double、void 這種 Native Type 與 Integer、Boolean、Double、Void 這種類別成員的差異,所以在 Kotlin 中類別名稱一律使用大寫開頭,例如 Int、Boolean、Double、Unit、Nothing 等等,其他基本上與 Java 極為相像,詳情請見 JetBrains 官方文件: [Coding Conventions for Kotlin](https://kotlinlang.org/docs/reference/coding-conventions.html) 64 | 65 | ## 最基礎必須要懂的 Kotlin 常用語法 66 | 67 | ### Variable 宣告 68 | - WarmApp.kt Line 25 69 | 70 | ``` 71 | 72 | // Java 寫法 73 | 74 | String message = "I am Ascii"; 75 | final Intent startServiceIntent = new Intent(this, WarmService.class); 76 | 77 | // Kotlin 寫法 78 | // 使用編譯器自動推導的形態時可省略 :Type 79 | // 例如 message 變數的 :String 可省略不寫 80 | 81 | var message = "I am Ascii" 82 | val startServiceIntent: Intent = Intent(this, WarmService::class.java) 83 | 84 | 85 | ``` 86 | 87 | ### static 成員使用 companion object 取代 88 | - WarmApp.kt Line 14 89 | 90 | ``` 91 | 92 | // Java 寫法 93 | 94 | public static final String DEFAULT_CHANNEL_ID = "WarmPackage"; 95 | 96 | // Kotlin 寫法 97 | 98 | companion object { 99 | val DEFAULT_CHANNEL_ID: String = "WarmPackage" 100 | } 101 | 102 | // 如果只是要定義常數,請加上 const 例如 103 | 104 | companion object { 105 | const val DEFAULT_CHANNEL_ID: String = "WarmPackage" 106 | } 107 | 108 | 109 | ``` 110 | 111 | ### Function 112 | - WarmApp.kt Line 34 113 | 114 | ``` 115 | 116 | // Java 寫法 117 | 118 | private void createNotificationChannel() { 119 | notificationManager.createNotificationChannel(notificationChannel); 120 | } 121 | 122 | // Kotlin 寫法 123 | 124 | private fun createNotificationChannel(): Unit { 125 | notificationManager.createNotificationChannel(notificationChannel) 126 | } 127 | 128 | 129 | ``` 130 | 131 | ### Function 參數的注意事項 132 | 133 | ``` 134 | 135 | fun main(args: Array) { 136 | var dd: Double = 0.0 137 | foo(dd) 138 | } 139 | 140 | fun foo(dParam: Double) { 141 | dParam = 99.9 // 編譯錯誤 Val cannot be reassigned 142 | } 143 | 144 | 145 | ``` 146 | 147 | ### Kotlin 操作 Java Class<T> 148 | - WarmApp.kt Line 43 149 | 150 | ``` 151 | 152 | // Java 寫法 153 | 154 | NotificationManager notificationManager = getSystemService(NotificationManager.class); 155 | 156 | // Kotlin 寫法 157 | 158 | val notificationManager = getSystemService(NotificationManager::class.java) 159 | 160 | 161 | ``` 162 | 163 | ### 繼承與實作 164 | - MainActivity.kt Line 21 165 | 166 | ``` 167 | 168 | // Java 寫法 169 | 170 | class MainActivity extends AppCompatActivity implements WarmPackageView { 171 | 172 | // override 採用 annotation 的方式指定 173 | @Override 174 | public void onResume() { 175 | super.onResume(); 176 | } 177 | 178 | } 179 | 180 | // Kotlin 寫法 181 | 182 | class MainActivity : AppCompatActivity(), WarmPackageView { 183 | 184 | // override 寫在 function 宣告中 185 | override fun onResume() { 186 | super.onResume() 187 | } 188 | 189 | } 190 | 191 | 192 | ``` 193 | 194 | ### final constant 成員 195 | - MainActivity.kt Line 23 196 | 197 | ``` 198 | 199 | // Java 寫法 200 | 201 | private final int MAX_TEMPERATURE = 50; 202 | 203 | // Kotlin 寫法 204 | // 採用 val 而非 var 宣告常數值 (val 是 value 的意思) 205 | 206 | private val MAX_TEMPERATURE:Int = 50 207 | 208 | 209 | ``` 210 | 211 | ### 可為 null 的變數 212 | - MainActivity.kt Line 25 213 | 214 | ``` 215 | 216 | // warmService 這個變數可為 null 217 | // 所有類別成員變數若在建構式前無法給值,一律需宣告為可 null 218 | 219 | private var warmService: WarmService? = null 220 | 221 | 222 | ``` 223 | 224 | ### 操作可為 null 的變數 225 | - MainActivity.kt Line 62 226 | 227 | ``` 228 | 229 | // 操作物件成員前要使用 ?. 230 | // 意指若 presenter 為 null 時會直接回傳 null 給 targetTemperature 231 | 232 | var targetTemperature:Int = presenter?.getTargetTemperature()?.toInt() ?: 0 233 | 234 | 235 | ``` 236 | 237 | ### 操作可 null 的變數時若需要預設值 238 | - MainActivity.kt Line 62 239 | 240 | ``` 241 | 242 | // 在 ?: 後提供左側任何環節發生 null 時該提供的預設值 243 | 244 | var targetTemperature:Int = presenter?.getTargetTemperature()?.toInt() ?: 0 245 | 246 | 247 | ``` 248 | 249 | ### if not null 判斷的寫法 250 | - MainActivity.kt Line 80 251 | 252 | ``` 253 | 254 | // Java 寫法 255 | 256 | if (warmService != null) { 257 | if (presenter != null) { 258 | presenter.attachModel(warmService); 259 | presenter.initial(); 260 | } 261 | } 262 | 263 | // Kotlin 寫法 264 | // 多行或需要對非 null 回傳值做運算時用 ?.let { ... } 265 | // 單行時用 ?. 就夠了 266 | 267 | warmService?.let { 268 | presenter?.attachModel(it) 269 | presenter?.initial() 270 | } 271 | 272 | 273 | ``` 274 | 275 | ### if null 判斷的寫法 276 | - MainActivity.kt Line 86 277 | 278 | ``` 279 | 280 | // Java 寫法 281 | 282 | if (warmService == null) { 283 | updateUIStatus(); 284 | } 285 | 286 | // Kotlin 寫法 287 | 288 | warmService ?: updateUIStatus() 289 | 290 | 291 | ``` 292 | 293 | ### ?.let 與 ?: 也可以搭著寫 294 | - MainActivity.kt Line 80 295 | 296 | ``` 297 | 298 | // Java 寫法 299 | 300 | if (warmService != null) { 301 | if (presenter != null) { 302 | presenter.attachModel(warmService); 303 | presenter.initial(); 304 | } 305 | } else { 306 | updateUIStatus(); 307 | } 308 | 309 | // Kotlin 寫法 310 | 311 | warmService?.let { 312 | presenter?.attachModel(it) 313 | presenter?.initial() 314 | } ?: updateUIStatus() 315 | 316 | 317 | ``` 318 | 319 | ### function return value 用於判斷式的限制 320 | - MainActivity.kt Line 114 321 | 322 | ``` 323 | 324 | // Java 寫法 325 | 326 | if (presenter.getIsRunning()) { 327 | Log.e(TAG, "is running") 328 | } 329 | 330 | // Kotlin 寫法 331 | // 因為 presenter 可能是 null 332 | // 所以不能像 Java 只寫 if (presenter.getIsRunning()) { ... } 333 | 334 | if (presenter?.getIsRunning() == true) { 335 | Log.e(TAG, "is running") 336 | } 337 | 338 | // 如果你對 Kotlin 的語法讀起來已經很順暢了,也可以這樣寫 339 | 340 | presenter?.takeIf { it.isRunning }?.apply { Log.e(TAG, "is running") } 341 | 342 | 343 | ``` 344 | 345 | ### 可為 null 變數用於判斷式的注意事項 346 | - MainActivity.kt Line 46 347 | 348 | ``` 349 | 350 | // 若 presenter 是 null 時,會直接回傳 null 351 | // 也就是說最終拿來判斷是否 == false 的不一定是 getIsRunning() 的值 352 | // 所以這時後你的判斷式如果是 if (presenter?.getIsRunning() != true) 353 | // 可能會發生 null != true 而進去流程的狀況 354 | // 請注意判斷式習慣正向判斷 == true 或 == false,別用 != 355 | 356 | if (presenter?.getIsRunning() == false) { 357 | presenter?.closeService() 358 | } 359 | 360 | 361 | ``` 362 | 363 | ### 更方便的字串語法 364 | - MainActivity.kt Line 94 365 | 366 | ``` 367 | 368 | // 在 "" 雙引號範圍內,可以利用 $ 符號來引入變數 369 | 370 | textviewTemperature.setText("$temperature") 371 | 372 | // 也可以使用 ${ 運算式 } 例如寫這樣也行 373 | 374 | textviewTemperature.setText("${initTargetTemp + progress}") 375 | 376 | // 當然也可以用在 String.format("") 中,相對於使用 %d 方便許多 377 | 378 | Log.e(tag, String.format("${initTargetTemp + progress}")) 379 | 380 | 381 | ``` 382 | 383 | ### Property 的概念 384 | - MainActivity.kt Line 123 385 | 386 | ``` 387 | 388 | // Java 寫法 389 | 390 | textviewCurrentTemperature.setText(currentTemp.toString()) 391 | 392 | // Kotlin 寫法 393 | // 在 Kotlin 內 getter、setter 是自動產生的 394 | // 所以直接操作 text 其實是操作了自動產生的 text setter 395 | // 而不是真的操作到 TextView 內的 text 這個 member 396 | 397 | textviewCurrentTemperature.text = currentTemp.toString() 398 | 399 | 400 | ``` 401 | 402 | ### Switch case 的寫法 403 | - MainActivity.kt Line 130 404 | 405 | ``` 406 | 407 | // 不像 Switch 有 break 可以用 408 | // 有多個條件成立的狀況是用逗號相接 409 | // 例如第二個可以寫成 false, null -> { ... } 410 | 411 | when (uiStatus) { 412 | true -> { 413 | fab.setEnabled(false) 414 | seekbarTemperature.setEnabled(false) 415 | createSnackBar() 416 | } 417 | false -> { 418 | fab.isEnabled = true 419 | seekbarTemperature.isEnabled = true 420 | closeSnackBar() 421 | } 422 | } 423 | 424 | 425 | ``` 426 | 427 | ### 沒有回傳值的 function 428 | - MainActivity.kt Line 150 429 | 430 | ``` 431 | 432 | // Java 寫法 433 | 434 | private void createSnackBar() { 435 | } 436 | 437 | // Kotlin 寫法 438 | // 然後 Unit 可以省略 439 | // 有一個很類似的類別叫 Nothing 可以混用但記得義意是不同的 440 | 441 | private fun createSnackBar(): Unit { 442 | } 443 | 444 | 445 | ``` 446 | 447 | ### 暱名物件的寫法 448 | - MainActivity.kt Line 153 449 | 450 | ``` 451 | 452 | // Java 寫法 453 | 454 | new View.OnClickListener() { 455 | 456 | @Override 457 | public void onClick(View view) { 458 | view.setEnabled(false); 459 | } 460 | 461 | } 462 | 463 | // Kotlin 寫法 464 | 465 | object : View.OnClickListener { 466 | 467 | override fun onClick(view: View?) { 468 | view?.setEnabled(false) 469 | } 470 | 471 | } 472 | 473 | 474 | ``` 475 | 476 | ### Android Context 操作中常用的 MainActivity.this 477 | - WarmService.kt Line 66 478 | 479 | ``` 480 | 481 | // Java 寫法 482 | 483 | return WarmService.this; 484 | 485 | // Kotlin 寫法 486 | 487 | return this@WarmService 488 | 489 | 490 | ``` 491 | 492 | ### 關於轉型 493 | - WarmService.kt Line 70 494 | 495 | ``` 496 | 497 | // Java 寫法 498 | 499 | SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); 500 | 501 | // Kotlin 寫法 502 | // 使用 as 轉型,而非在前面使用 (Type) 503 | // 如果想避免 CaseException 也可以改用 as? 在無法轉型時回傳 null 504 | 505 | var sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager 506 | 507 | 508 | ``` 509 | 510 | ### for 用於 List 物件的寫法 511 | - WarmService.kt Line 88 512 | 513 | ``` 514 | 515 | // 正常是寫 for (sensor:Sensor in allSensor) 516 | // 但 Kotlin 可自動辨示所以型態可以省略 517 | 518 | for (sensor in allSensor) { 519 | Log.e("Sensor", sensor.toString()) 520 | if (sensor.name.toLowerCase().indexOf("temp") >= 0) { 521 | temperatureSensor = sensor 522 | break 523 | } 524 | } 525 | 526 | 527 | ``` 528 | 529 | ### for 用於整數計算的寫法 530 | - WarmService.kt Line 113 531 | 532 | ``` 533 | 534 | // for (int i=1; i<=30; ++i) 的意思 535 | 536 | for (i in 1..30) { 537 | try { 538 | var runnable = WarmRunnable() 539 | threadList.add(runnable) 540 | Thread(runnable).start() 541 | } catch (e: Exception) { 542 | break 543 | } 544 | } 545 | 546 | 547 | ``` 548 | 549 | ### 預設類別建構式 550 | - APIBase.kt Line 11 551 | 552 | ``` 553 | 554 | // 預設建構式有指定參數時,就不再擁有無參數的建構式了 555 | // 以這個例子來看就是 queue: ResquestQueue 556 | // 所以無法用 var api = InvoiceAPI() 來建構,一定要餵個 queue object 557 | 558 | abstract class APIBase, ResultType>(queue: RequestQueue): API 559 | 560 | 561 | ``` 562 | 563 | ### 類別成員與建構式參數 564 | - APIBase.kt Line 14 565 | 566 | ``` 567 | 568 | // 類別成員宣告時可以直接調用建構式中的參數 569 | 570 | private var requestQueue: RequestQueue = queue 571 | 572 | // 如果有要在建構式做其他事的話就是醬寫 573 | // 需要注意 init function 位置必須在操作到的成員變數之下,否則無法編譯 574 | 575 | private var requestQueue: RequestQueue? = null 576 | 577 | init { 578 | requestQueue = queue 579 | } 580 | 581 | 582 | ``` 583 | 584 | ### 類別建構式 Overloading 585 | - InvoiceAPI.kt Line 12 586 | 587 | ``` 588 | 589 | // 如果需要寫第二種建構式就是醬寫 590 | // 預設建構式以外的建構式都一定要傳入預設建構式所需的參數 591 | // 並且使用 : this(參數) 來執行預設建構式 592 | // 以此為例就是 queue:RequestQueue 593 | // 如果拿掉 queue:RequestQueue 就會無法編譯 594 | 595 | constructor(queue:RequestQueue, 596 | successListener: API.APISuccessListener, 597 | failListener: API.APIFailListener): this(queue) { 598 | this.successListener = successListener 599 | this.failListener = failListener 600 | } 601 | 602 | 603 | ``` 604 | 605 | ### Functional programming 風格的語法 606 | - InvoiceResult.kt Line 20 607 | 608 | ``` 609 | 610 | // Java 寫法 611 | 612 | @Override 613 | String getUrl() { 614 | return "https://asciihuang.github.io/invoice.json"; 615 | } 616 | 617 | // Kotlin 寫法 618 | // 可以省略 { return "..." } function body 619 | // 背後的概念是指函數可以被賦值 620 | 621 | override fun getUrl(): String = "https://asciihuang.github.io/invoice.json" 622 | 623 | 624 | ``` 625 | 626 | ## 由反組譯了解 Kotlin 語法 627 | 628 | ### Null Safety 629 | 630 | ``` 631 | 632 | // Kotlin 寫法 633 | 634 | private fun closeSnackBar() { 635 | snackbar?.dismiss() 636 | } 637 | 638 | // Compile 後 639 | 640 | private final void closeSnackBar() 641 | { 642 | Snackbar localSnackbar = this.snackbar; 643 | if (localSnackbar != null) { 644 | localSnackbar.dismiss(); 645 | } 646 | } 647 | 648 | 649 | ``` 650 | 651 | ### companion object 652 | 653 | ``` 654 | 655 | // Kotlin 寫法 656 | 657 | companion object { 658 | val DEFAULT_CHANNEL_ID: String = "WarmPackage" 659 | } 660 | 661 | // Compile 後 662 | 663 | public static final Companion Companion = new Companion(null); 664 | 665 | @NotNull 666 | public static final String DEFAULT_CHANNEL_ID = "WarmPackage"; 667 | 668 | @Metadata(bv={1, 0, 2}, d1={"\000\024\n\002\030\002\n\002\020\000\n\002\b\002\n\002\020\016\n\002\b\003\b��\003\030\0002\0020\001B\007\b\002��\006\002\020\002R\024\020\003\032\0020\004X��D��\006\b\n\000\032\004\b\005\020\006��\006\007"}, d2={"Lcom/ascii/warmpackage/WarmApp$Companion;", "", "()V", "DEFAULT_CHANNEL_ID", "", "getDEFAULT_CHANNEL_ID", "()Ljava/lang/String;", "app_debug"}, k=1, mv={1, 1, 7}) 669 | public static final class Companion 670 | { 671 | @NotNull 672 | public final String getDEFAULT_CHANNEL_ID() 673 | { 674 | return WarmApp.access$getDEFAULT_CHANNEL_ID$cp(); 675 | } 676 | } 677 | 678 | 679 | ``` 680 | 681 | ### Class<T> 682 | 683 | ``` 684 | 685 | // Kotlin 寫法 686 | 687 | val startServiceIntent: Intent = Intent(this, WarmService::class.java) 688 | 689 | // Compile 後 690 | 691 | Intent localIntent = new Intent((Context)this, WarmService.class); 692 | 693 | 694 | ``` 695 | 696 | ### for (i in 1..30) 697 | 698 | ``` 699 | 700 | // Kotlin 寫法 701 | 702 | for (i in 1..30) { 703 | try { 704 | var runnable = WarmRunnable() 705 | threadList.add(runnable) 706 | Thread(runnable).start() 707 | } catch (e: Exception) { 708 | break 709 | } 710 | } 711 | 712 | // Compile 後 713 | 714 | int i = 1; 715 | while (i < 31) { 716 | try 717 | { 718 | WarmRunnable localWarmRunnable = new WarmRunnable(); 719 | this.threadList.add(localWarmRunnable); 720 | new Thread((Runnable)localWarmRunnable).start(); 721 | i++; 722 | } 723 | catch (Exception localException) {} 724 | } 725 | 726 | 727 | ``` 728 | 729 | ### 操作 Companion Object 730 | 731 | ``` 732 | 733 | // Kotlin 寫法 734 | 735 | val mBuilder = 736 | NotificationCompat.Builder(this, WarmApp.DEFAULT_CHANNEL_ID) 737 | 738 | // Compile 後 739 | 740 | NotificationCompat.Builder localBuilder = 741 | new NotificationCompat.Builder((Context)this, 742 | WarmApp.Companion.getDEFAULT_CHANNEL_ID()) 743 | 744 | 745 | ``` 746 | 747 | ### 字串處理語法 748 | 749 | ``` 750 | 751 | // Kotlin 寫法 752 | 753 | val contentText:String = String.format("$currentTemperature / ${getTargetTemperature()}") 754 | 755 | // Compile 後 756 | 757 | String str1 = "" + this.currentTemperature + " / " + getTargetTemperature(); 758 | 759 | 760 | ``` 761 | 762 | ### Safety call 與預設值 763 | 764 | 765 | ``` 766 | 767 | // Kotlin 寫法 768 | 769 | var dValue = presenter?.getTargetTemperature() ?: 99.0 770 | textviewTemperature.setText(dValue.toString()) 771 | 772 | var sValue = warmService?.getMd5Hash("12345") ?: "Ascii" 773 | textviewTemperature.setText(sValue) 774 | 775 | // Compile 後 776 | 777 | WarmPackagePresenter localWarmPackagePresenter = this.presenter; 778 | double d; 779 | String str; 780 | if (localWarmPackagePresenter != null) 781 | { 782 | Double localDouble = localWarmPackagePresenter.getTargetTemperature(); 783 | if (localDouble != null) 784 | { 785 | d = localDouble.doubleValue(); 786 | ((TextView)_$_findCachedViewById(R.id.textviewTemperature)).setText((CharSequence)String.valueOf(d)); 787 | WarmService localWarmService = this.warmService; 788 | if (localWarmService == null) { 789 | break label115; 790 | } 791 | str = localWarmService.getMd5Hash("12345"); 792 | if (str == null) { 793 | break label115; 794 | } 795 | } 796 | } 797 | for (;;) 798 | { 799 | ((TextView)_$_findCachedViewById(R.id.textviewTemperature)).setText((CharSequence)str); 800 | return; 801 | d = 99.0D; 802 | break; 803 | label115: 804 | str = "Ascii"; 805 | } 806 | 807 | 808 | ``` 809 | 810 | ### Unit 與 Nothing 811 | 812 | ``` 813 | 814 | // Kotlin 寫法 815 | 816 | fun testUnit(): Unit 817 | fun testNothing(): Nothing 818 | 819 | // Compile 後 820 | 821 | @NotNull 822 | public abstract Void testNothing(); 823 | public abstract void testUnit(); 824 | 825 | 826 | ``` 827 | 828 | 829 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 26 9 | defaultConfig { 10 | applicationId "com.ascii.warmpackage" 11 | minSdkVersion 16 12 | targetSdkVersion 26 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: 'libs', include: ['*.jar']) 27 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" 28 | implementation 'com.android.support:appcompat-v7:26.1.0' 29 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 30 | implementation 'com.android.support:design:26.1.0' 31 | implementation 'com.google.code.gson:gson:2.8.4' 32 | implementation 'com.android.volley:volley:1.1.0' 33 | testImplementation 'junit:junit:4.12' 34 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 35 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 36 | } 37 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/ascii/warmpackage/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ascii.warmpackage 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.ascii.warmpackage", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/ascii/warmpackage/Base64.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2013 KKBOX Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * ​http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | /** 16 | * Base64 encoder/decoder 17 | */ 18 | package com.ascii.warmpackage; 19 | 20 | public class Base64 { 21 | 22 | private static char[] map1 = new char[64]; 23 | static { 24 | int i = 0; 25 | for (char c = 'A'; c <= 'Z'; c++) 26 | map1[i++] = c; 27 | for (char c = 'a'; c <= 'z'; c++) 28 | map1[i++] = c; 29 | for (char c = '0'; c <= '9'; c++) 30 | map1[i++] = c; 31 | map1[i++] = '+'; 32 | map1[i++] = '/'; 33 | } 34 | 35 | private static byte[] map2 = new byte[128]; 36 | static { 37 | for (int i = 0; i < map2.length; i++) 38 | map2[i] = -1; 39 | for (int i = 0; i < 64; i++) 40 | map2[map1[i]] = (byte) i; 41 | } 42 | 43 | public static String encodeString(byte[] in){ 44 | return new String(encode(in)); 45 | } 46 | 47 | public static String encodeString(String s) { 48 | return new String(encode(s.getBytes())); 49 | } 50 | 51 | public static char[] encode(byte[] in) { 52 | return encode(in, in.length); 53 | } 54 | 55 | /** 56 | * encode() : Base64 encoding with length 57 | * @param in 58 | * @param iLen 59 | * @return 60 | */ 61 | public static char[] encode(byte[] in, int iLen) { 62 | int oDataLen = (iLen * 4 + 2) / 3; // output length without padding 63 | int oLen = ((iLen + 2) / 3) * 4; // output length including padding 64 | char[] out = new char[oLen]; 65 | int ip = 0; 66 | int op = 0; 67 | while (ip < iLen) { 68 | int i0 = in[ip++] & 0xff; 69 | int i1 = ip < iLen ? in[ip++] & 0xff : 0; 70 | int i2 = ip < iLen ? in[ip++] & 0xff : 0; 71 | int o0 = i0 >>> 2; 72 | int o1 = ((i0 & 3) << 4) | (i1 >>> 4); 73 | int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); 74 | int o3 = i2 & 0x3F; 75 | out[op++] = map1[o0]; 76 | out[op++] = map1[o1]; 77 | out[op] = op < oDataLen ? map1[o2] : '='; 78 | op++; 79 | out[op] = op < oDataLen ? map1[o3] : '='; 80 | op++; 81 | } 82 | return out; 83 | } 84 | 85 | /** 86 | * decodeString() : Decode string from base64. 87 | * @param s : String to decode 88 | * @return 89 | */ 90 | public static String decodeString(String s) { 91 | return new String(decode(s)); 92 | } 93 | 94 | public static byte[] decode(String s) { 95 | return decode(s.toCharArray()); 96 | } 97 | 98 | public static byte[] decode(char[] in) { 99 | int iLen = in.length; 100 | if (iLen % 4 != 0) 101 | throw new IllegalArgumentException( 102 | "Length of Base64 encoded input string is not a multiple of 4."); 103 | while (iLen > 0 && in[iLen - 1] == '=') 104 | iLen--; 105 | int oLen = (iLen * 3) / 4; 106 | byte[] out = new byte[oLen]; 107 | int ip = 0; 108 | int op = 0; 109 | while (ip < iLen) { 110 | int i0 = in[ip++]; 111 | int i1 = in[ip++]; 112 | int i2 = ip < iLen ? in[ip++] : 'A'; 113 | int i3 = ip < iLen ? in[ip++] : 'A'; 114 | if (i0 > 127 || i1 > 127 || i2 > 127 || i3 > 127) 115 | throw new IllegalArgumentException( 116 | "Illegal character in Base64 encoded data."); 117 | int b0 = map2[i0]; 118 | int b1 = map2[i1]; 119 | int b2 = map2[i2]; 120 | int b3 = map2[i3]; 121 | if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0) 122 | throw new IllegalArgumentException( 123 | "Illegal character in Base64 encoded data."); 124 | int o0 = (b0 << 2) | (b1 >>> 4); 125 | int o1 = ((b1 & 0xf) << 4) | (b2 >>> 2); 126 | int o2 = ((b2 & 3) << 6) | b3; 127 | out[op++] = (byte) o0; 128 | if (op < oLen) 129 | out[op++] = (byte) o1; 130 | if (op < oLen) 131 | out[op++] = (byte) o2; 132 | } 133 | return out; 134 | } 135 | 136 | private Base64() { 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /app/src/main/java/com/ascii/warmpackage/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ascii.warmpackage 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.ServiceConnection 7 | import android.graphics.Color 8 | import android.os.Bundle 9 | import android.os.IBinder 10 | import android.support.design.widget.Snackbar 11 | import android.support.v7.app.AppCompatActivity 12 | import android.view.View 13 | import android.widget.SeekBar 14 | import com.ascii.warmpackage.model.WarmService 15 | import com.ascii.warmpackage.presenter.MainPresenter 16 | import com.ascii.warmpackage.presenter.WarmPackagePresenter 17 | 18 | import kotlinx.android.synthetic.main.activity_main.* 19 | import kotlinx.android.synthetic.main.content_main.* 20 | 21 | class MainActivity : AppCompatActivity(), WarmPackageView { 22 | 23 | private val MAX_TEMPERATURE:Int = 50 24 | 25 | private var warmService: WarmService? = null 26 | private var presenter: WarmPackagePresenter? =null 27 | private var parentView: View? = null 28 | private var initTargetTemp: Int = 0 29 | private var snackbar: Snackbar? = null 30 | 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | super.onCreate(savedInstanceState) 33 | presenter = MainPresenter(this) 34 | setContentView(R.layout.activity_main) 35 | setSupportActionBar(toolbar) 36 | seekbarTemperature.setOnSeekBarChangeListener(seekBarChangeListener) 37 | parentView = findViewById(R.id.parent_view) 38 | fab.setOnClickListener { view -> 39 | ( presenter?.startWarm() ) 40 | } 41 | } 42 | 43 | override fun onDestroy() { 44 | super.onDestroy() 45 | // 想想這裡如果寫 != true 會有什麼危險 46 | if (presenter?.getIsRunning() == false) { 47 | presenter?.closeService() 48 | } 49 | } 50 | 51 | override fun onResume() { 52 | super.onResume() 53 | bindService(Intent(this, WarmService::class.java), serviceConnectionListener, Context.BIND_AUTO_CREATE) 54 | } 55 | 56 | override fun onPause() { 57 | super.onPause() 58 | unbindService(serviceConnectionListener) 59 | } 60 | 61 | override fun initUIStatus() { 62 | var targetTemperature:Int = presenter?.getTargetTemperature()?.toInt() ?: 0 63 | textviewTemperature.setText("" + targetTemperature) 64 | updateUIStatus() 65 | } 66 | 67 | // private ServiceConnection serviceConnectionListener = new ServiceConnection() 的 Kotlin 寫法 68 | private val serviceConnectionListener = object: ServiceConnection { 69 | override fun onServiceDisconnected(className: ComponentName?) { 70 | presenter?.detachModel() 71 | warmService = null 72 | } 73 | 74 | override fun onServiceConnected(className: ComponentName?, service: IBinder?) { 75 | val binder = service as WarmService.WarmBinder 76 | warmService = binder.getService() 77 | // if (persenter != null) { ... } 的簡潔寫法 78 | // 若不需回傳值,或只有一個指令要操作,直接用 warmService?.foo() 即可 79 | // 若需要回傳值,就要改用 ?.let { it } 語法來寫,可保平安之外又不失簡潔 80 | warmService?.let { 81 | presenter?.attachModel(it) 82 | presenter?.initial() 83 | } 84 | 85 | // if (warmService == null) 的意思 86 | warmService ?: updateUIStatus() 87 | } 88 | } 89 | 90 | private var seekBarChangeListener = object: SeekBar.OnSeekBarChangeListener { 91 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { 92 | if (fromUser) { 93 | var temperature = initTargetTemp + progress 94 | textviewTemperature.setText("$temperature") 95 | presenter?.setTargetTemperature(temperature.toDouble()) 96 | } 97 | } 98 | 99 | override fun onStartTrackingTouch(seekBar: SeekBar?) { 100 | } 101 | 102 | override fun onStopTrackingTouch(seekBar: SeekBar?) { 103 | } 104 | } 105 | 106 | override fun updateCurrentTemperature(currentTemp: Double) { 107 | if (initTargetTemp == 0) { 108 | initTargetTemp = Math.min(currentTemp.toInt() + 5, MAX_TEMPERATURE) 109 | var tempMax = MAX_TEMPERATURE - initTargetTemp 110 | seekbarTemperature.setMax(tempMax) 111 | textviewTemperature.setText("" + initTargetTemp) 112 | 113 | // 因為 presenter 可能是 null 所以不能只寫 if (presenter?.getIsRunning()) 114 | if (presenter?.getIsRunning() == true) { 115 | var targetTemperature:Int = presenter?.getTargetTemperature()?.toInt() ?: 0 116 | var progress:Int = targetTemperature - initTargetTemp 117 | seekbarTemperature.setProgress(progress) 118 | textviewTemperature.setText("" + targetTemperature) 119 | } else { 120 | presenter?.setTargetTemperature(initTargetTemp.toDouble()) 121 | } 122 | } 123 | textviewCurrentTemperature.text = currentTemp.toString() 124 | } 125 | 126 | override fun updateUIStatus() { 127 | var uiStatus: Boolean = presenter?.getIsRunning() ?: false 128 | 129 | // 這是 Kotlin 的 Switch case 130 | when (uiStatus) { 131 | true -> { 132 | // Kotlin 有 Property (屬性) 的概念 133 | // 所以像這種 Java 風格的 get/set 在 Kotlin 中會希望你改用 Property 來操作 134 | fab.setEnabled(false) 135 | seekbarTemperature.setEnabled(false) 136 | createSnackBar() 137 | } 138 | false -> { 139 | // isEnabled 是一個 Property 140 | // 作用等價於 Java 的 setEnabled(true) 141 | fab.isEnabled = true 142 | seekbarTemperature.isEnabled = true 143 | closeSnackBar() 144 | } 145 | } 146 | } 147 | 148 | // Unit 就是 void 的意思,可省略不寫 149 | // 也可以寫 Nothing 但意義不同 150 | private fun createSnackBar(): Unit { 151 | // object: View.OnClickListener 是 new View.OnClickListener 最直接的翻譯 152 | snackbar = Snackbar.make(parentView!!, R.string.running, Snackbar.LENGTH_INDEFINITE) 153 | .setAction(R.string.action_exit, object : View.OnClickListener { 154 | override fun onClick(v: View?) { 155 | presenter?.stopWarm() 156 | } 157 | }) 158 | snackbar?.let { 159 | it.setActionTextColor(Color.WHITE) 160 | it.show() 161 | } 162 | } 163 | 164 | private fun closeSnackBar() { 165 | snackbar?.dismiss() 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /app/src/main/java/com/ascii/warmpackage/WarmApp.kt: -------------------------------------------------------------------------------- 1 | package com.ascii.warmpackage 2 | 3 | import android.annotation.TargetApi 4 | import android.app.Application 5 | import android.app.Notification 6 | import android.app.NotificationChannel 7 | import android.app.NotificationManager 8 | import android.content.Context 9 | import android.content.Intent 10 | import android.os.Build 11 | import com.ascii.warmpackage.model.WarmService 12 | 13 | class WarmApp : Application() { 14 | 15 | companion object { 16 | val DEFAULT_CHANNEL_ID: String = "WarmPackage" 17 | } 18 | 19 | override fun onCreate() { 20 | super.onCreate() 21 | startWarmService() 22 | } 23 | 24 | private fun startWarmService() { 25 | val startServiceIntent: Intent = Intent(this, WarmService::class.java) 26 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 27 | startForegroundService(startServiceIntent) 28 | } 29 | createNotificationChannel() 30 | startService(startServiceIntent) 31 | } 32 | 33 | @TargetApi(Build.VERSION_CODES.O) 34 | private fun createNotificationChannel(): Unit { 35 | var notificationChannel = NotificationChannel(DEFAULT_CHANNEL_ID, "WarmPackage", NotificationManager.IMPORTANCE_LOW) 36 | notificationChannel.setDescription("WarmPackage") 37 | notificationChannel.setShowBadge(true) 38 | notificationChannel.enableVibration(false) 39 | notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC) 40 | // 這樣子也可以拿 NotificationManager 41 | // val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 42 | // 不過我想示範一下 Kotlin 怎麼操作 Java Class 43 | val notificationManager = getSystemService(NotificationManager::class.java) 44 | notificationManager.createNotificationChannel(notificationChannel) 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ascii/warmpackage/WarmPackageView.kt: -------------------------------------------------------------------------------- 1 | package com.ascii.warmpackage 2 | 3 | interface WarmPackageView { 4 | fun initUIStatus() 5 | fun updateCurrentTemperature(currentTemp: Double) 6 | fun updateUIStatus() 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ascii/warmpackage/api/API.kt: -------------------------------------------------------------------------------- 1 | package com.ascii.warmpackage.api 2 | 3 | interface API { 4 | fun getUrl(): String 5 | fun parseResult(data: String): ResultType 6 | fun success(listener: APISuccessListener): API 7 | fun fail(listener: APIFailListener): API 8 | 9 | interface APISuccessListener { 10 | fun onSuccess(result: ResultType) 11 | } 12 | 13 | interface APIFailListener { 14 | fun onFail() 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ascii/warmpackage/api/APIBase.kt: -------------------------------------------------------------------------------- 1 | package com.ascii.warmpackage.api 2 | 3 | import com.android.volley.Request 4 | import com.android.volley.RequestQueue 5 | import com.android.volley.Response 6 | import com.android.volley.VolleyError 7 | import com.android.volley.toolbox.StringRequest 8 | 9 | // 將子類別的型態、結果類別的型態指定為泛型 10 | // 並指定傳入的第一個類別只能是 APIBase 的子類 11 | abstract class APIBase, ResultType>(queue: RequestQueue): API { 12 | 13 | // 類別成員宣告時可以直接調用建構式中的參數 14 | private var requestQueue: RequestQueue = queue 15 | // 如果有要在建構式做其他事的話就是醬寫 16 | // 需要注意 init function 位置必須在操作到的成員變數之下,否則無法編譯 17 | // 可以試著把 init { ... } 整段搬到最上面試試 18 | // init { 19 | // requestQueue = queue 20 | // } 21 | 22 | protected var successListener: API.APISuccessListener? = null 23 | protected var failListener: API.APIFailListener? = null 24 | 25 | override fun getUrl(): String { 26 | return "" 27 | } 28 | 29 | override fun success(listener: API.APISuccessListener): APIType { 30 | successListener = listener 31 | return this as APIType 32 | } 33 | 34 | override fun fail(listener: API.APIFailListener): APIType { 35 | failListener = listener 36 | return this as APIType 37 | } 38 | 39 | fun start() { 40 | var url = getUrl() 41 | if ("".equals(url)) { 42 | failListener?.onFail() 43 | } else { 44 | var stringRequest = StringRequest(Request.Method.GET, getUrl(), 45 | object : Response.Listener { 46 | override fun onResponse(response: String?) { 47 | response?.let { 48 | var result: ResultType = parseResult(it) 49 | successListener?.onSuccess(result) 50 | } 51 | } 52 | }, 53 | object : Response.ErrorListener { 54 | override fun onErrorResponse(error: VolleyError?) { 55 | failListener?.onFail() 56 | } 57 | }) 58 | requestQueue.add(stringRequest) 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ascii/warmpackage/api/InvoiceAPI.kt: -------------------------------------------------------------------------------- 1 | package com.ascii.warmpackage.api 2 | 3 | import com.android.volley.* 4 | import com.google.gson.Gson 5 | 6 | class InvoiceAPI(queue: RequestQueue) : APIBase(queue) { 7 | 8 | // 如果需要寫第二種建構式就是醬寫 9 | // 預設建構式以外的建構式都一定要傳入預設建構式所需的參數 10 | // 以此為例就是 queue:RequestQueue 11 | // 如果拿掉 queue:RequestQueue 就會無法編譯 12 | constructor(queue:RequestQueue, 13 | successListener: API.APISuccessListener, 14 | failListener: API.APIFailListener): this(queue) { 15 | this.successListener = successListener 16 | this.failListener = failListener 17 | } 18 | 19 | // 省略 { return "..." } function body 的寫法 20 | override fun getUrl(): String = "https://asciihuang.github.io/invoice.json" 21 | 22 | override fun parseResult(data: String): InvoiceResult { 23 | var receiptResult = Gson().fromJson(data, InvoiceResult::class.java) 24 | return receiptResult 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ascii/warmpackage/api/InvoiceResult.kt: -------------------------------------------------------------------------------- 1 | package com.ascii.warmpackage.api 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class InvoiceResult { 6 | @SerializedName("receipts") 7 | var receipts: List? = null 8 | 9 | class InvoiceEntity { 10 | @SerializedName("item") 11 | var item: String? = null 12 | 13 | @SerializedName("value") 14 | var value: String? = null 15 | } 16 | 17 | override fun toString(): String { 18 | var stringBuilder = StringBuilder() 19 | receipts?.let { 20 | for (receipt in it) { 21 | stringBuilder.appendln("${receipt.item}: ${receipt.value}") 22 | } 23 | } 24 | return stringBuilder.toString() 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ascii/warmpackage/model/WarmPackageModel.kt: -------------------------------------------------------------------------------- 1 | package com.ascii.warmpackage.model 2 | 3 | interface WarmPackageModel { 4 | fun startWarm() 5 | fun stopWarm() 6 | fun getIsRunning(): Boolean 7 | fun setTemperatureSensorUpdateListener(listener: TemperatureSensorUpdate) 8 | fun setTargetTemperature(temperature: Double) 9 | fun getTargetTemperature(): Double 10 | fun closeService() 11 | 12 | interface TemperatureSensorUpdate { 13 | fun update(temperature: Double) 14 | fun notifyTargetArrival() 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ascii/warmpackage/model/WarmService.kt: -------------------------------------------------------------------------------- 1 | package com.ascii.warmpackage.model 2 | 3 | import android.app.NotificationManager 4 | import android.app.Service 5 | import android.content.Intent 6 | import android.os.Binder 7 | import android.os.IBinder 8 | import android.app.PendingIntent 9 | import android.content.Context 10 | import android.hardware.Sensor 11 | import android.hardware.SensorEvent 12 | import android.hardware.SensorEventListener 13 | import android.hardware.SensorManager 14 | import android.support.v4.app.NotificationCompat 15 | import android.util.Log 16 | import java.math.BigInteger 17 | import java.security.MessageDigest 18 | import java.security.NoSuchAlgorithmException 19 | import com.android.volley.toolbox.Volley 20 | import com.android.volley.RequestQueue 21 | import com.ascii.warmpackage.Base64 22 | import com.ascii.warmpackage.MainActivity 23 | import com.ascii.warmpackage.R 24 | import com.ascii.warmpackage.WarmApp 25 | import com.ascii.warmpackage.api.API 26 | import com.ascii.warmpackage.api.InvoiceAPI 27 | import com.ascii.warmpackage.api.InvoiceResult 28 | 29 | class WarmService : Service(), WarmPackageModel { 30 | 31 | private val WARM_NOTIFICATION: Int = 0 32 | private val warmBinder = WarmBinder() 33 | private var isRunning = false 34 | 35 | // Kotlin 所有的型別名字都是大寫開頭,沒有 double 與 Double 之分 36 | private var currentTemperature: Double = 0.0 37 | private var targetTemperature: Double = 0.0 38 | private var temperatureSensor: Sensor? = null 39 | private var temperatureUpdateListener: WarmPackageModel.TemperatureSensorUpdate? = null 40 | private var threadList: ArrayList = ArrayList() 41 | private var requestQueue: RequestQueue? = null 42 | 43 | override fun onCreate() { 44 | super.onCreate() 45 | stopWarm() 46 | setUpSensor() 47 | cancelNotification() 48 | requestQueue = Volley.newRequestQueue(this) 49 | } 50 | 51 | override fun onBind(p0: Intent?): IBinder { 52 | return warmBinder 53 | } 54 | 55 | override fun onDestroy() { 56 | super.onDestroy() 57 | stopWarm() 58 | cancelNotification() 59 | var sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager 60 | sensorManager.unregisterListener(sensorEventListener) 61 | temperatureUpdateListener = null 62 | } 63 | 64 | inner class WarmBinder : Binder() { 65 | fun getService(): WarmService { 66 | return this@WarmService 67 | } 68 | } 69 | 70 | private fun setUpSensor() { 71 | // Kotlin 的轉型是用 as 72 | // 如果想避免 CaseException 也可以改用 as? 在無法轉型時回傳 null 73 | var sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager 74 | 75 | // 理論上這樣拿得到溫度 Sensor 76 | temperatureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_AMBIENT_TEMPERATURE) 77 | 78 | if (temperatureSensor == null) { 79 | // 實際上很多機器要這樣才拿得到 (然後 htc 是兩種方式都拿不到) 80 | // 這裡示範了 ?: 運算式 81 | // 若不為空就得到左側的東西 82 | // 若為空就得到右側的東西 83 | // 而 emptyList() 是 Kotlin 的語法糖 84 | var allSensor:List = sensorManager.getSensorList(Sensor.TYPE_ALL) ?: emptyList() 85 | 86 | // 正常是寫 for (sensor:Sensor in allSensor) 87 | // 但 Kotlin 可自動辨示所以型態可以省略 88 | for (sensor in allSensor) { 89 | Log.e("Sensor", sensor.toString()) 90 | if (sensor.name.toLowerCase().indexOf("temp") >= 0) { 91 | temperatureSensor = sensor 92 | break 93 | } 94 | } 95 | } 96 | 97 | // if (temperatureSensor != null) { ... } 的簡潔寫法 98 | temperatureSensor?.let { 99 | sensorManager.registerListener(sensorEventListener, temperatureSensor, SensorManager.SENSOR_DELAY_NORMAL) 100 | } 101 | } 102 | 103 | override fun startWarm() { 104 | isRunning = true 105 | threadList.clear() 106 | createWarmThread() 107 | createAndUpdateNotification(true) 108 | demoAPICall() 109 | } 110 | 111 | private fun createWarmThread() { 112 | // for (int i=1; i<=30; ++i) 的意思 113 | for (i in 1..30) { 114 | try { 115 | var runnable = WarmRunnable() 116 | threadList.add(runnable) 117 | Thread(runnable).start() 118 | } catch (e: Exception) { 119 | break 120 | } 121 | } 122 | } 123 | 124 | private fun demoAPICall() { 125 | // if (requestQueue != null) { ... } 的意思 126 | requestQueue?.let { 127 | // 使用預設建構式 128 | InvoiceAPI(it).success(object: API.APISuccessListener { 129 | override fun onSuccess(result: InvoiceResult) { 130 | Log.e("Invoice API - First", result.toString()) 131 | } 132 | }).fail(object: API.APIFailListener { 133 | override fun onFail() { 134 | Log.e("Invoice API - First", "Fail") 135 | } 136 | }).start() 137 | 138 | // 使用 second constructor 139 | var api = InvoiceAPI(it, 140 | object: API.APISuccessListener { 141 | override fun onSuccess(result: InvoiceResult) { 142 | Log.e("Invoice API - Second", result.toString()) 143 | } 144 | }, 145 | object: API.APIFailListener { 146 | override fun onFail() { 147 | Log.e("Invoice API - Second", "Fail") 148 | } 149 | }) 150 | api.start() 151 | } 152 | } 153 | 154 | override fun stopWarm() { 155 | for (runnable in threadList) { 156 | runnable.stop = true 157 | } 158 | cancelNotification() 159 | threadList.clear() 160 | isRunning = false 161 | } 162 | 163 | override fun getIsRunning(): Boolean { 164 | return isRunning 165 | } 166 | 167 | override fun setTemperatureSensorUpdateListener(listener: WarmPackageModel.TemperatureSensorUpdate) { 168 | temperatureUpdateListener = listener 169 | } 170 | 171 | override fun setTargetTemperature(temperature: Double) { 172 | targetTemperature = temperature 173 | } 174 | 175 | // private SensorEventListener sensorEventListener = new SensorEventListener() 的意思 176 | private var sensorEventListener = object: SensorEventListener { 177 | override fun onSensorChanged(event: SensorEvent) { 178 | var temperature: Double = event.values[0] + 0.05 179 | var temp: Int = (temperature * 10).toInt() 180 | temperature = temp / 10.0 181 | if (currentTemperature != temperature) { 182 | currentTemperature = temperature 183 | temperatureUpdateListener?.update(currentTemperature) 184 | if (currentTemperature >= targetTemperature) { 185 | stopWarm() 186 | temperatureUpdateListener?.notifyTargetArrival() 187 | } 188 | if (isRunning) { 189 | createAndUpdateNotification(false) 190 | } 191 | } 192 | } 193 | 194 | override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { 195 | } 196 | } 197 | 198 | override fun getTargetTemperature(): Double { 199 | return targetTemperature 200 | } 201 | 202 | override fun closeService() { 203 | stopSelf() 204 | } 205 | 206 | private fun createAndUpdateNotification(first: Boolean) { 207 | val contentText:String = String.format("$currentTemperature / ${getTargetTemperature()}") 208 | val notificationIntent = Intent(this, MainActivity::class.java) 209 | val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0) 210 | 211 | // 在 Kotlin 中沒有 static 的用法 212 | // 要用 companion object + const 的方式來取代 213 | // 請至 WarmApp 中參考 DEFAULT_CHANNEL_ID 的宣告方式 214 | val mBuilder = NotificationCompat.Builder(this, WarmApp.DEFAULT_CHANNEL_ID) 215 | .setSmallIcon(R.mipmap.ic_launcher) 216 | .setContentTitle(getString(R.string.running)) 217 | .setContentText(contentText) 218 | .setOngoing(true) 219 | .setContentIntent(pendingIntent) 220 | 221 | val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 222 | var notification = mBuilder.build() 223 | notificationManager.notify(WARM_NOTIFICATION, notification) 224 | if (first) { 225 | startForeground(WARM_NOTIFICATION, notification) 226 | } 227 | } 228 | 229 | private fun cancelNotification() { 230 | val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 231 | notificationManager.cancel(WARM_NOTIFICATION) 232 | } 233 | 234 | fun getMd5Hash(input: String): String? { 235 | try { 236 | val md = MessageDigest.getInstance("MD5") 237 | val messageDigest = md.digest(input.toByteArray()) 238 | val number = BigInteger(1, messageDigest) 239 | var md5 = number.toString(16) 240 | while (md5.length < 32) { 241 | md5 = "0$md5" 242 | } 243 | return md5 244 | } catch (e: NoSuchAlgorithmException) { 245 | return null 246 | } 247 | } 248 | 249 | inner class WarmRunnable: Runnable { 250 | var stop: Boolean = false 251 | override fun run() { 252 | Log.e("Warm", "Thread Started") 253 | while (!stop) { 254 | var time = System.currentTimeMillis() 255 | var timeMD5Base64 = getMd5Hash(Base64.encodeString(System.currentTimeMillis().toString())) 256 | if (time % 5000 == 0L) { 257 | Log.e("MD5", timeMD5Base64) 258 | } 259 | } 260 | Log.e("Warm", "Thread Stoped") 261 | } 262 | } 263 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ascii/warmpackage/presenter/MainPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.ascii.warmpackage.presenter 2 | 3 | import com.ascii.warmpackage.WarmPackageView 4 | import com.ascii.warmpackage.model.WarmPackageModel 5 | 6 | class MainPresenter(view: WarmPackageView): WarmPackagePresenter { 7 | 8 | private var view: WarmPackageView?= null 9 | private var model: WarmPackageModel?= null 10 | 11 | init { 12 | this.view = view 13 | } 14 | 15 | override fun attachModel(model: WarmPackageModel) { 16 | this.model = model 17 | } 18 | 19 | override fun detachModel() { 20 | model = null 21 | } 22 | 23 | override fun getIsRunning(): Boolean { 24 | return model?.getIsRunning() ?: false 25 | } 26 | 27 | override fun startWarm() { 28 | model?.startWarm() 29 | view?.updateUIStatus() 30 | } 31 | 32 | override fun initial() { 33 | model?.setTemperatureSensorUpdateListener(temperatureUpdateListener) 34 | view?.initUIStatus() 35 | } 36 | 37 | override fun stopWarm() { 38 | model?.stopWarm() 39 | view?.updateUIStatus() 40 | } 41 | 42 | override fun setTargetTemperature(temperature: Double) { 43 | model?.setTargetTemperature(temperature) 44 | } 45 | 46 | override fun getTargetTemperature(): Double? { 47 | return model?.getTargetTemperature() 48 | } 49 | 50 | override fun closeService() { 51 | model?.closeService() 52 | } 53 | 54 | private var temperatureUpdateListener = object: WarmPackageModel.TemperatureSensorUpdate { 55 | override fun notifyTargetArrival() { 56 | view.updateUIStatus() 57 | } 58 | 59 | override fun update(temperature: Double) { 60 | view.updateCurrentTemperature(temperature) 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ascii/warmpackage/presenter/WarmPackagePresenter.kt: -------------------------------------------------------------------------------- 1 | package com.ascii.warmpackage.presenter 2 | 3 | import com.ascii.warmpackage.model.WarmPackageModel 4 | 5 | interface WarmPackagePresenter { 6 | fun initial() 7 | fun attachModel(model: WarmPackageModel) 8 | fun detachModel() 9 | fun startWarm() 10 | fun stopWarm() 11 | fun getIsRunning(): Boolean 12 | fun setTargetTemperature(temperature: Double) 13 | fun getTargetTemperature(): Double? 14 | fun closeService() 15 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 18 | 19 | 24 | 25 | 31 | 32 | 37 | 38 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsciiHuang/warm-package/f77fbe6a146c849d431f17a1942280ca22d8cdc4/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsciiHuang/warm-package/f77fbe6a146c849d431f17a1942280ca22d8cdc4/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsciiHuang/warm-package/f77fbe6a146c849d431f17a1942280ca22d8cdc4/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsciiHuang/warm-package/f77fbe6a146c849d431f17a1942280ca22d8cdc4/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsciiHuang/warm-package/f77fbe6a146c849d431f17a1942280ca22d8cdc4/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsciiHuang/warm-package/f77fbe6a146c849d431f17a1942280ca22d8cdc4/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsciiHuang/warm-package/f77fbe6a146c849d431f17a1942280ca22d8cdc4/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsciiHuang/warm-package/f77fbe6a146c849d431f17a1942280ca22d8cdc4/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsciiHuang/warm-package/f77fbe6a146c849d431f17a1942280ca22d8cdc4/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsciiHuang/warm-package/f77fbe6a146c849d431f17a1942280ca22d8cdc4/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 16dp 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Warm Package 3 | About 4 | 關閉 5 | 目前溫度 6 | 設定發熱溫度 7 | 發熱中… 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 |