├── LICENSE.md ├── README.md ├── RxJava.md ├── SUMMARY.md ├── accountmanager.md ├── activity-fragment.md ├── adb.md ├── akka.md ├── android_sdk.md ├── android_support_annotations.md ├── annotation_programming.md ├── aspect.md ├── assert.md ├── authors.md ├── autovalue.md ├── bolts-android.md ├── book.json ├── clean_architecture.md ├── cover.jpg ├── cover.png ├── cover.svg ├── dagger2.md ├── dagger2.png ├── dagger2.svg ├── debug_stetho_,_leakcanary.md ├── dev.md ├── docker.md ├── effective_android.md ├── eventbus-otto.md ├── facebook_sdk.md ├── faq.md ├── firebase.md ├── firebase_update.jpg ├── flow_mortar.md ├── gradle.md ├── image_loader_android-universal-image-loader,_picasso,_glide,_fresco.md ├── javadoc.md ├── json_to_pojo.md ├── kai_yuan_ba_gua.md ├── kotlin.md ├── lambda.md ├── loadermanager.md ├── mockito.md ├── multidex.md ├── notification.md ├── openlibrary.md ├── openpackage_jitpack_jcenter_mavencentral.md ├── orm_-_activeandroid,_dbflow,_ollie.md ├── parse.md ├── proguard.md ├── react_native.md ├── recyclerview_twowayview.md ├── resources.md ├── retrofit.md ├── rx-operator.md ├── rxandroid.md ├── rxbinding.md ├── setup-wizard.md ├── stream.md ├── styles └── website.css ├── test_-_espresso,_rxpresso.md ├── test_-_robolectric.md ├── theme.md └── zh-cn ├── README.md ├── SUMMARY.md └── rxandroid.md /LICENSE.md: -------------------------------------------------------------------------------- 1 | Attribution 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More_considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution 4.0 International Public License 58 | 59 | By exercising the Licensed Rights (defined below), You accept and agree 60 | to be bound by the terms and conditions of this Creative Commons 61 | Attribution 4.0 International Public License ("Public License"). To the 62 | extent this Public License may be interpreted as a contract, You are 63 | granted the Licensed Rights in consideration of Your acceptance of 64 | these terms and conditions, and the Licensor grants You such rights in 65 | consideration of benefits the Licensor receives from making the 66 | Licensed Material available under these terms and conditions. 67 | 68 | 69 | Section 1 -- Definitions. 70 | 71 | a. Adapted Material means material subject to Copyright and Similar 72 | Rights that is derived from or based upon the Licensed Material 73 | and in which the Licensed Material is translated, altered, 74 | arranged, transformed, or otherwise modified in a manner requiring 75 | permission under the Copyright and Similar Rights held by the 76 | Licensor. For purposes of this Public License, where the Licensed 77 | Material is a musical work, performance, or sound recording, 78 | Adapted Material is always produced where the Licensed Material is 79 | synched in timed relation with a moving image. 80 | 81 | b. Adapter's License means the license You apply to Your Copyright 82 | and Similar Rights in Your contributions to Adapted Material in 83 | accordance with the terms and conditions of this Public License. 84 | 85 | c. Copyright and Similar Rights means copyright and/or similar rights 86 | closely related to copyright including, without limitation, 87 | performance, broadcast, sound recording, and Sui Generis Database 88 | Rights, without regard to how the rights are labeled or 89 | categorized. For purposes of this Public License, the rights 90 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 91 | Rights. 92 | 93 | d. Effective Technological Measures means those measures that, in the 94 | absence of proper authority, may not be circumvented under laws 95 | fulfilling obligations under Article 11 of the WIPO Copyright 96 | Treaty adopted on December 20, 1996, and/or similar international 97 | agreements. 98 | 99 | e. Exceptions and Limitations means fair use, fair dealing, and/or 100 | any other exception or limitation to Copyright and Similar Rights 101 | that applies to Your use of the Licensed Material. 102 | 103 | f. Licensed Material means the artistic or literary work, database, 104 | or other material to which the Licensor applied this Public 105 | License. 106 | 107 | g. Licensed Rights means the rights granted to You subject to the 108 | terms and conditions of this Public License, which are limited to 109 | all Copyright and Similar Rights that apply to Your use of the 110 | Licensed Material and that the Licensor has authority to license. 111 | 112 | h. Licensor means the individual(s) or entity(ies) granting rights 113 | under this Public License. 114 | 115 | i. Share means to provide material to the public by any means or 116 | process that requires permission under the Licensed Rights, such 117 | as reproduction, public display, public performance, distribution, 118 | dissemination, communication, or importation, and to make material 119 | available to the public including in ways that members of the 120 | public may access the material from a place and at a time 121 | individually chosen by them. 122 | 123 | j. Sui Generis Database Rights means rights other than copyright 124 | resulting from Directive 96/9/EC of the European Parliament and of 125 | the Council of 11 March 1996 on the legal protection of databases, 126 | as amended and/or succeeded, as well as other essentially 127 | equivalent rights anywhere in the world. 128 | 129 | k. You means the individual or entity exercising the Licensed Rights 130 | under this Public License. Your has a corresponding meaning. 131 | 132 | 133 | Section 2 -- Scope. 134 | 135 | a. License grant. 136 | 137 | 1. Subject to the terms and conditions of this Public License, 138 | the Licensor hereby grants You a worldwide, royalty-free, 139 | non-sublicensable, non-exclusive, irrevocable license to 140 | exercise the Licensed Rights in the Licensed Material to: 141 | 142 | a. reproduce and Share the Licensed Material, in whole or 143 | in part; and 144 | 145 | b. produce, reproduce, and Share Adapted Material. 146 | 147 | 2. Exceptions and Limitations. For the avoidance of doubt, where 148 | Exceptions and Limitations apply to Your use, this Public 149 | License does not apply, and You do not need to comply with 150 | its terms and conditions. 151 | 152 | 3. Term. The term of this Public License is specified in Section 153 | 6(a). 154 | 155 | 4. Media and formats; technical modifications allowed. The 156 | Licensor authorizes You to exercise the Licensed Rights in 157 | all media and formats whether now known or hereafter created, 158 | and to make technical modifications necessary to do so. The 159 | Licensor waives and/or agrees not to assert any right or 160 | authority to forbid You from making technical modifications 161 | necessary to exercise the Licensed Rights, including 162 | technical modifications necessary to circumvent Effective 163 | Technological Measures. For purposes of this Public License, 164 | simply making modifications authorized by this Section 2(a) 165 | (4) never produces Adapted Material. 166 | 167 | 5. Downstream recipients. 168 | 169 | a. Offer from the Licensor -- Licensed Material. Every 170 | recipient of the Licensed Material automatically 171 | receives an offer from the Licensor to exercise the 172 | Licensed Rights under the terms and conditions of this 173 | Public License. 174 | 175 | b. No downstream restrictions. You may not offer or impose 176 | any additional or different terms or conditions on, or 177 | apply any Effective Technological Measures to, the 178 | Licensed Material if doing so restricts exercise of the 179 | Licensed Rights by any recipient of the Licensed 180 | Material. 181 | 182 | 6. No endorsement. Nothing in this Public License constitutes or 183 | may be construed as permission to assert or imply that You 184 | are, or that Your use of the Licensed Material is, connected 185 | with, or sponsored, endorsed, or granted official status by, 186 | the Licensor or others designated to receive attribution as 187 | provided in Section 3(a)(1)(A)(i). 188 | 189 | b. Other rights. 190 | 191 | 1. Moral rights, such as the right of integrity, are not 192 | licensed under this Public License, nor are publicity, 193 | privacy, and/or other similar personality rights; however, to 194 | the extent possible, the Licensor waives and/or agrees not to 195 | assert any such rights held by the Licensor to the limited 196 | extent necessary to allow You to exercise the Licensed 197 | Rights, but not otherwise. 198 | 199 | 2. Patent and trademark rights are not licensed under this 200 | Public License. 201 | 202 | 3. To the extent possible, the Licensor waives any right to 203 | collect royalties from You for the exercise of the Licensed 204 | Rights, whether directly or through a collecting society 205 | under any voluntary or waivable statutory or compulsory 206 | licensing scheme. In all other cases the Licensor expressly 207 | reserves any right to collect such royalties. 208 | 209 | 210 | Section 3 -- License Conditions. 211 | 212 | Your exercise of the Licensed Rights is expressly made subject to the 213 | following conditions. 214 | 215 | a. Attribution. 216 | 217 | 1. If You Share the Licensed Material (including in modified 218 | form), You must: 219 | 220 | a. retain the following if it is supplied by the Licensor 221 | with the Licensed Material: 222 | 223 | i. identification of the creator(s) of the Licensed 224 | Material and any others designated to receive 225 | attribution, in any reasonable manner requested by 226 | the Licensor (including by pseudonym if 227 | designated); 228 | 229 | ii. a copyright notice; 230 | 231 | iii. a notice that refers to this Public License; 232 | 233 | iv. a notice that refers to the disclaimer of 234 | warranties; 235 | 236 | v. a URI or hyperlink to the Licensed Material to the 237 | extent reasonably practicable; 238 | 239 | b. indicate if You modified the Licensed Material and 240 | retain an indication of any previous modifications; and 241 | 242 | c. indicate the Licensed Material is licensed under this 243 | Public License, and include the text of, or the URI or 244 | hyperlink to, this Public License. 245 | 246 | 2. You may satisfy the conditions in Section 3(a)(1) in any 247 | reasonable manner based on the medium, means, and context in 248 | which You Share the Licensed Material. For example, it may be 249 | reasonable to satisfy the conditions by providing a URI or 250 | hyperlink to a resource that includes the required 251 | information. 252 | 253 | 3. If requested by the Licensor, You must remove any of the 254 | information required by Section 3(a)(1)(A) to the extent 255 | reasonably practicable. 256 | 257 | 4. If You Share Adapted Material You produce, the Adapter's 258 | License You apply must not prevent recipients of the Adapted 259 | Material from complying with this Public License. 260 | 261 | 262 | Section 4 -- Sui Generis Database Rights. 263 | 264 | Where the Licensed Rights include Sui Generis Database Rights that 265 | apply to Your use of the Licensed Material: 266 | 267 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 268 | to extract, reuse, reproduce, and Share all or a substantial 269 | portion of the contents of the database; 270 | 271 | b. if You include all or a substantial portion of the database 272 | contents in a database in which You have Sui Generis Database 273 | Rights, then the database in which You have Sui Generis Database 274 | Rights (but not its individual contents) is Adapted Material; and 275 | 276 | c. You must comply with the conditions in Section 3(a) if You Share 277 | all or a substantial portion of the contents of the database. 278 | 279 | For the avoidance of doubt, this Section 4 supplements and does not 280 | replace Your obligations under this Public License where the Licensed 281 | Rights include other Copyright and Similar Rights. 282 | 283 | 284 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 285 | 286 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 287 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 288 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 289 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 290 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 291 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 292 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 293 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 294 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 295 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 296 | 297 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 298 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 299 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 300 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 301 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 302 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 303 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 304 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 305 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 306 | 307 | c. The disclaimer of warranties and limitation of liability provided 308 | above shall be interpreted in a manner that, to the extent 309 | possible, most closely approximates an absolute disclaimer and 310 | waiver of all liability. 311 | 312 | 313 | Section 6 -- Term and Termination. 314 | 315 | a. This Public License applies for the term of the Copyright and 316 | Similar Rights licensed here. However, if You fail to comply with 317 | this Public License, then Your rights under this Public License 318 | terminate automatically. 319 | 320 | b. Where Your right to use the Licensed Material has terminated under 321 | Section 6(a), it reinstates: 322 | 323 | 1. automatically as of the date the violation is cured, provided 324 | it is cured within 30 days of Your discovery of the 325 | violation; or 326 | 327 | 2. upon express reinstatement by the Licensor. 328 | 329 | For the avoidance of doubt, this Section 6(b) does not affect any 330 | right the Licensor may have to seek remedies for Your violations 331 | of this Public License. 332 | 333 | c. For the avoidance of doubt, the Licensor may also offer the 334 | Licensed Material under separate terms or conditions or stop 335 | distributing the Licensed Material at any time; however, doing so 336 | will not terminate this Public License. 337 | 338 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 339 | License. 340 | 341 | 342 | Section 7 -- Other Terms and Conditions. 343 | 344 | a. The Licensor shall not be bound by any additional or different 345 | terms or conditions communicated by You unless expressly agreed. 346 | 347 | b. Any arrangements, understandings, or agreements regarding the 348 | Licensed Material not stated herein are separate from and 349 | independent of the terms and conditions of this Public License. 350 | 351 | 352 | Section 8 -- Interpretation. 353 | 354 | a. For the avoidance of doubt, this Public License does not, and 355 | shall not be interpreted to, reduce, limit, restrict, or impose 356 | conditions on any use of the Licensed Material that could lawfully 357 | be made without permission under this Public License. 358 | 359 | b. To the extent possible, if any provision of this Public License is 360 | deemed unenforceable, it shall be automatically reformed to the 361 | minimum extent necessary to make it enforceable. If the provision 362 | cannot be reformed, it shall be severed from this Public License 363 | without affecting the enforceability of the remaining terms and 364 | conditions. 365 | 366 | c. No term or condition of this Public License will be waived and no 367 | failure to comply consented to unless expressly agreed to by the 368 | Licensor. 369 | 370 | d. Nothing in this Public License constitutes or may be interpreted 371 | as a limitation upon, or waiver of, any privileges and immunities 372 | that apply to the Licensor or You, including from the legal 373 | processes of any jurisdiction or authority. 374 | 375 | 376 | ======================================================================= 377 | 378 | Creative Commons is not a party to its public licenses. 379 | Notwithstanding, Creative Commons may elect to apply one of its public 380 | licenses to material it publishes and in those instances will be 381 | considered the "Licensor." Except for the limited purpose of indicating 382 | that material is shared under a Creative Commons public license or as 383 | otherwise permitted by the Creative Commons policies published at 384 | creativecommons.org/policies, Creative Commons does not authorize the 385 | use of the trademark "Creative Commons" or any other trademark or logo 386 | of Creative Commons without its prior written consent including, 387 | without limitation, in connection with any unauthorized modifications 388 | to any of its public licenses or any other arrangements, 389 | understandings, or agreements concerning use of licensed material. For 390 | the avoidance of doubt, this paragraph does not form part of the public 391 | licenses. 392 | 393 | Creative Commons may be contacted at creativecommons.org. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 開源安卓 2 | 3 | * gitbook: http://yongjhih.gitbooks.io/feed/ 4 | 5 | [![](cover.jpg)](http://yongjhih.gitbooks.io/feed/) 6 | 7 | 12 | 13 | * github: https://github.com/yongjhih/android-gitbook/ 14 | 15 | 開放書籍,歡迎共筆修改,以 gitbook 方式瀏覽時,可點擊頁面上方的編輯頁面連結 "EDIT THIS PAGE" 即可共筆。 16 | 17 | 善用開源來開發 Android App 。不只是開放源碼,也要使用開放資源來開發 Android App。 18 | 19 | 腦補閱讀,以少量文字描述,減少筆者主觀介入,盡可能聚焦在對照程式碼上。 20 | 21 | 前端: 22 | 23 | * RxJava, RxAndroid 24 | * retrolambda (java7 + java8) 25 | * retrofit, [yongjhih/NotRetrofit](https://github.com/yongjhih/NotRetrofit) 26 | * okhttp 27 | * DI: Dagger2 28 | * AutoParcel, AutoValue, etc. 29 | * ImageLoader: fresco, AUIL, picasso, glide, etc. 30 | * json2pojo: jackson, gson, logansquare, etc. 31 | * Orm: DBFlow, etc. 32 | * SimpleFacebook, [yongjhih/RetroFacebook](https://github.com/yongjhih/RetroFacebook) 33 | * EventBus/Otto 34 | * mockito 35 | * espresso 36 | * robolectric 37 | * assertj(fest), truth 38 | * icepick 39 | * [yongjhih/auto-parse](https://github.com/yongjhih/auto-parse) 40 | * [yongjhih/RxParse](https://github.com/yongjhih/RxParse) 41 | * [yongjhih/RxFacebook](https://github.com/yongjhih/RxFacebook) 42 | * [yongjhih/proguard annotations](https://github.com/yongjhih/proguard-annotations) 43 | * [yongjhih/proguard snippets](https://github.com/yongjhih/proguard-snippets) 44 | * [yongjhih/json2notification](https://github.com/8tory/json2notification) 45 | * kotlin 46 | * anko 47 | * ReactNative 48 | * akka 49 | * jitpack 50 | * bintray 51 | * bountysource 52 | * gitter 53 | * travis-ci 54 | 55 | 後端: 56 | 57 | * Parse BAAS 58 | 59 | 開發環境: 60 | 61 | * Android Studio 62 | * gradle 63 | 64 | 專案環境: 65 | 66 | * gitlab 67 | * phabricator 68 | 69 | 持續整合環境: 70 | 71 | * jenkins 72 | 73 | ## 版權 74 | 75 | [![CC-BY 4.0](http://creativecommons.tw/sites/creativecommons.tw/files/cc-by.png)](https://creativecommons.org/licenses/by/4.0/legalcode.txt) 76 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Introduction](README.md) 4 | * [RxJava](RxJava.md) 5 | * [RxAndroid](rxandroid.md) 6 | * [RxBinding](rxbinding.md) 7 | * [開發 RxJava 新增自己的 Operator](rx-operator.md) 8 | * [資料流替代方案](stream.md) 9 | * [Lambda](lambda.md) 10 | * [Bolts-Android](bolts-android.md) 11 | * [AutoValue](autovalue.md) 12 | * [Dagger2](dagger2.md) 13 | * [Annotation Programming](annotation_programming.md) 14 | * [RecyclerView, TwoWayView](recyclerview_twowayview.md) 15 | * [Android Support Annotations](android_support_annotations.md) 16 | * [Image Loader - Android-Universal-Image-Loader, picasso, glide, fresco](image_loader_android-universal-image-loader,_picasso,_glide,_fresco.md) 17 | * [json to POJO](json_to_pojo.md) 18 | * [Clean architecture](clean_architecture.md) 19 | * [Retrofit](retrofit.md) 20 | * [Orm - ActiveAndroid, DBFlow, Ollie](orm_-_activeandroid,_dbflow,_ollie.md) 21 | * [flow + mortar](flow_mortar.md) 22 | * [EventBus: greenrobot/EventBus, Square/otto](eventbus-otto.md) 23 | * [良好的撰寫習慣 JavaDoc](javadoc.md) 24 | * [開源套件中心 - jitpack/jcenter/mavenCentral](openpackage_jitpack_jcenter_mavencentral.md) 25 | * [開源碼函式庫](openlibrary.md) 26 | * [開發環境](dev.md) 27 | * [Test - Assert 斷言 - assertj, truth](assert.md) 28 | * [Test - Mockito](mockito.md) 29 | * [Test - UI - espresso, rxpresso](test_-_espresso,_rxpresso.md) 30 | * [Debug 除錯 - stetho, leakcanary](debug_stetho_,_leakcanary.md) 31 | * [Test - robolectric](test_-_robolectric.md) 32 | * [Kotlin](kotlin.md) 33 | * [Aspect](aspect.md) 34 | * [AccountManager](accountmanager.md) 35 | * [Notification](notification.md) 36 | * [開源八卦](kai_yuan_ba_gua.md) 37 | * [經典問題 - 指引你「這該怎麼弄?」](faq.md) 38 | * [作者群介紹](authors.md) 39 | * [docker](docker.md) 40 | * [資源](resources.md) 41 | * [Parse](parse.md) 42 | * [gradle](gradle.md) 43 | * [React Native](react_native.md) 44 | * [Android SDK](android_sdk.md) 45 | * [adb](adb.md) 46 | * [Theme](theme.md) 47 | * [Grooid](grooid.md) 48 | * [MultiDex](multidex.md) 49 | * [Proguard](proguard.md) 50 | * [LoaderManager](loadermanager.md) 51 | * [akka](akka.md) 52 | * [Android 的日常](effective_android.md) 53 | * [Activity 與 Fragment 的日常](activity-fragment.md) 54 | * [Binder 活頁夾的日常](binder.md) 55 | * [Background Thread 幕後的日常](background-thread.md) 56 | * [Firebase](firebase.md) 57 | * [系統 - 安裝精靈](setup-wizard.md) 58 | 59 | -------------------------------------------------------------------------------- /accountmanager.md: -------------------------------------------------------------------------------- 1 | # AccountManager 2 | 3 | 使用者帳號服務。 4 | 5 | 你可以以你的 app 名義透過 AccountManager 建立屬於你的使用者。 6 | 7 | 建立帳號(Top Down): 8 | 9 | ```java 10 | accountManager.addAccountExplicitly((Account) account, "password", (Bundle) userdata); // nullable userdata 11 | 12 | Account account = new Account(username, accountType); 13 | 14 | String accountType = "com.infstory"; // 帳號識別類型 15 | String username = "yongjhih@example.com"; // 帳號名稱 16 | ``` 17 | 18 | 相當然爾,你可以提供登入畫面,透過網路服務確定登入成功後,再執行上方程式來建立本地使用者。個資以及帳密都會存在系統裡,僅有 app 擁有者本身才可以存取密碼。(*當然已經鮮少 app 會頻繁的使用密碼,一般後端的使用者授權服務,大多登入成功後,會提供一組暫時授權證書,大多操作皆使用此授權證書*) 19 | 20 | 從系統中取得帳號資料(userdata, 帳密與授權證書需要 app 擁有者才可以存取): 21 | 22 | ```java 23 | Account[] accounts = accountManager.getAccountsByType(accountType); 24 | Account account = accounts.length != 0 ? accounts[0]; 25 | ``` 26 | 27 | 設定授權證書(accessToken/authToken): 28 | 29 | ```java 30 | accountManager.setAuthToken(account, authTokenType, authToken); 31 | ``` 32 | 33 | 取得授權證書(accessToken/authToken): 34 | 35 | ```java 36 | String authToken = accountManager.peekAuthToken(account, authTokenType) 37 | ``` 38 | 39 | ## GitHub App 40 | 41 | 我們可以寫一個 GitHub App ,GitHub 網頁登入後,建立一個 GitHub 帳號,把 access token 存進去。 42 | 43 | ```java 44 | public class LoginActivity extends Activity { 45 | @Override public void onResume() { 46 | super.onResume(); 47 | 48 | loginButton.onClick(() -> { 49 | final GitHub github = GitHub.create(); 50 | 51 | // Populate username, password, clientId, clientSecret from UI 52 | // .. 53 | 54 | AppObservable.bindActivity(this, github.getAccessToken(username, password, clientId, clientSecret)) 55 | .subscribeOn(Scheduler.io()) 56 | .subscribe(accessToken -> { 57 | String accountType = "com.github"; 58 | Account account = new Account(username, "com.github"); 59 | AccountManager accountManager = AccountManager.get(context); 60 | accountManager.addAccountExplicitly(account, password, userdata); 61 | // 一般會寫 "user,user:email" 等,在 oauth 稱為 scope, 不過帳密授權應該就全部權限,這邊特別寫明是密碼登入,方便識別。 62 | String authType = "password"; // authType/scope/permissions 63 | String authToken = accessToken.accessToken; 64 | accountManager.setAuthToken(account, authType, authToken); 65 | 66 | finish(); 67 | }); 68 | }); 69 | } 70 | } 71 | ``` 72 | 73 | ```java 74 | @Retrofit("https://api.github.com") 75 | public abstract class GitHub { 76 | @FormUrlEncoded 77 | @POST("/oauth/token?grant_type=password") Observable getAccessToken( 78 | @Field("username") String email, 79 | @Field("password") String password, 80 | @Field("client_id") String clientId, 81 | @Field("client_secret") String clientSecret); 82 | public static GitHub create() { return new Retrofit_GitHub(); } 83 | } 84 | ``` 85 | 86 | ## 提供 3rd-party app 授權能力,充當行動裝置版本的 oauth provider 授權服務 87 | 88 | 3rd-party app 只要依據你帳號管理中心的 accountType 作為使用者中心/授權服務的識別名稱,呼叫即可透過系統叫起你的登入畫面,以提供授權證書。 89 | 90 | ```java 91 | AccountManagerFuture authTokenFutureBundle = accountManager.getAuthToken(account, authTokenType, (Bundle) null, activity, (AccountManagerCallback) null, (Handler) null); 92 | String authToken = authTokenFutureBundle.getString(AccountManager.KEY_AUTHTOKEN); // waiting for UI logon 93 | ``` 94 | 95 | GitHub App 就要比較辛苦的扮演使用者中心: 96 | 97 | * 提供帳號管理中心 98 | * 以及其登入畫面 99 | * 向 AccountManager 建立使用者 100 | 101 | 先從剛熟悉的登入畫面開始,不只是單純的自己登入就好,要想辦法把 authToken 傳給委託人。 102 | 103 | 改繼承 AccountAuthenticatorActivity 讓它幫你把一些你不想知道的東西塞好,如果你是用 AppCompatActivity 之類的,請參考 [AccountAuthenticatorActivity.java](https://github.com/android/platform_frameworks_base/blob/master/core/java/android/accounts/AccountAuthenticatorActivity.java) 源碼,自己塞。 104 | 105 | ```java 106 | public class LoginActivity extends AccountAuthenticatorActivity { // 1. 107 | @Override public void onResume() { 108 | super.onResume(); 109 | 110 | loginButton.onClick(() -> { 111 | final GitHub github = GitHub.create(); 112 | 113 | // .. 114 | 115 | AppObservable.bindActivity(this, github.getAccessToken(username, password, clientId, clientSecret)) 116 | .subscribeOn(Scheduler.io()) 117 | .subscribe(accessToken -> { 118 | String accountType = "com.github"; 119 | Account account = new Account(username, "com.github"); 120 | AccountManager accountManager = AccountManager.get(context); 121 | accountManager.addAccountExplicitly(account, password, userdata); 122 | String authType = "password"; 123 | String authToken = accessToken.accessToken; 124 | accountManager.setAuthToken(account, authType, authToken); 125 | 126 | // 2. 127 | Intent intent = new Intent(); 128 | intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, username); 129 | intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, accountType); 130 | intent.putExtra(AccountManager.KEY_AUTHTOKEN, authToken); 131 | ((AccountAuthenticatorActivity) this).setAccountAuthenticatorResult(intent.getExtras()); 132 | setResult(RESULT_OK, intent); 133 | 134 | finish(); 135 | }); 136 | }); 137 | } 138 | } 139 | ``` 140 | 141 | 委託登入授權服務(Authenticator) 實現所有 AccountManager 相關的代理操作。 142 | 143 | 3rd-party 對 AccountManager.getAuthToken() 其實會委託給我們的登入授權服務 Authenticator 受理 ,這部份我們再叫出剛配置好的登入畫面: 144 | 145 | ```java 146 | // 登入授權服務 147 | public class GitHubAuthenticator extends AbstractAccountAuthenticator { 148 | // 取得授權 149 | @Override 150 | public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, 151 | String authTokenType, Bundle options) throws NetworkErrorException { 152 | // 1. 你可以無條件提供授權 153 | // 2. 或者你可以提供一個畫面提醒使用者,是否同意提供授權 154 | 155 | // 如果還沒登入請使用者登入完再跳回來,繼續授權。 156 | 157 | AccountManager accountManager = AccountManager.get(context); 158 | // 拿拿看既有 GitHub Service 的權限授權 159 | String authToken = accountManager.peekAuthToken(account, authTokenType); 160 | 161 | String accountType = "com.github"; 162 | if (authToken != null) { 163 | Bundle bundle = new Bundle(); 164 | bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); 165 | bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType); 166 | bundle.putString(AccountManager.KEY_AUTHTOKEN, authToken); 167 | return bundle; 168 | } 169 | String authType = "password"; 170 | 171 | // 拿不到 authToken 就當作登入吧 (由於目前我們的政策是,登入後,授權已經塞好在 bundle 了,所以我們可以直接回傳) 172 | return addAccount(response, accountType, authType, (String[]) null, (Bundle) null); 173 | 174 | } 175 | 176 | // 建立帳號 177 | @Override 178 | public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, 179 | String authTokenType, String[] requiredFeatures, Bundle options) 180 | throws NetworkErrorException { 181 | 182 | // 登入畫面 183 | Intent intent = new Intent(context, LoginActivity.class); 184 | intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); 185 | 186 | final Bundle bundle = new Bundle(); 187 | bundle.putParcelable(AccountManager.KEY_INTENT, intent); 188 | return bundle; 189 | } 190 | } 191 | ``` 192 | 193 | 公開授權服務 AndroidManifest.xml: 194 | 195 | ```xml 196 | 199 | 200 | 201 | 202 | 205 | 206 | 207 | 211 | ``` 212 | 213 | res/xml/authenticator.xml: 214 | 215 | ```xml 216 | 221 | ``` 222 | 223 | 為了聽 AccountManagerService 發出來的委託 Service: 224 | 225 | ```java 226 | public class GitHubAuthenticatorService extends Service { 227 | @Override 228 | public IBinder onBind(Intent intent) { 229 | return new GitHubAuthenticator(this).getIBinder(); 230 | } 231 | } 232 | ``` 233 | 234 | ## 搭配 NotRetrofit 自動授權 235 | 236 | ```java 237 | GitHub github = GitHub.create(context); 238 | github.repos().subscribe(System.out::println); 239 | ``` 240 | 241 | ```java 242 | public interface GitHub { 243 | @RequestInterceptor(GitHubAuthInterceptor.class) 244 | @GET("/{owner}/repos") 245 | Observable repos(@Path String owner); 246 | } 247 | ``` 248 | 249 | 在呼叫 `GitHub.repos()` 時,會先呼叫 `accountManager.getAuthToken()` ,拿到 token 後,才憑證發出 `GET /{owner}/repos` request 。 250 | 251 | ```java 252 | @Singleton 253 | public class GitHubAuthInterceptor extends AuthenticationInterceptor { 254 | 255 | @Override 256 | public String accountType() { 257 | return "com.github"; 258 | } 259 | 260 | @Override 261 | public String authTokenType() { 262 | return "com.github"; 263 | } 264 | 265 | @Override 266 | public void intercept(String token, RequestFacade request) { 267 | if (token != null) request.addHeader("Authorization", "Bearer " + token); 268 | } 269 | 270 | } 271 | ``` 272 | 273 | ## 搭配 retroauth 自動授權 274 | 275 | ```java 276 | @Authentication(accountType = R.string.auth_account_type, tokenType = R.string.auth_token_type) 277 | public interface GitHub { 278 | @Authenticated 279 | @GET("/{owner}/repos") 280 | Observable repos(@Path String owner); 281 | } 282 | ``` 283 | 284 | ## retroauth 分析 285 | 286 | [AuthInvoker.java#L59](https://github.com/Unic8/retroauth/blob/master/retroauth/src/main/java/eu/unicate/retroauth/AuthInvoker.java#L59) 287 | 288 | ```java 289 | getAccountName() 290 | .flatMap(this::getAccount) 291 | .flatMap(this::getAuthToken) // 取得授權證 292 | .flatMap(this::authenticate) // 設定憑證 293 | .flatMap(o -> request) 294 | .retry((c, e) -> retryRule.retry(c, e)); 295 | ``` 296 | -------------------------------------------------------------------------------- /activity-fragment.md: -------------------------------------------------------------------------------- 1 | # Activity 與 Fragment 的日常 2 | 3 | 相當於一篇網頁。 4 | 5 | 依據使用者的操作頁面的操作,定義了一些狀態,稱之 Activity 的生命週期。 6 | 7 | Activity 生命週期: 8 | 9 | ``` 10 | |-onCreate() 11 | ||-onStart() 12 | |||-onResume() 13 | |||-onPause() 14 | ||-onStop() 15 | |-onDestroy() 16 | ``` 17 | 18 | 它們是對稱的存在。 19 | 20 | ``` 21 | |-onCreate() 第一次建立 22 | ||-onStart() 如果透過超連結回來的,從這開始 23 | |||-onResume() 在最上層顯示的時候 24 | |||-onPause() 被蓋在下層的時候 25 | ||-onStop() 被閒置很久的時候 26 | |-onDestroy() 被關掉的時候 27 | ``` 28 | 29 | 網路上有很多生命週期圖可以看,這裡就不附了。 30 | 31 | `onCreate()` 慣例是請你準備好/初始化的東西,像是 `setContentView(@Layout int layoutId)` ,沒做還會爆炸,這點在後來才提供的 Fragment 類別有簡單的防呆 `@NonNull View onCreateView();` 32 | 33 | 34 | -------------------------------------------------------------------------------- /adb.md: -------------------------------------------------------------------------------- 1 | # adb 2 | 3 | 安裝 apk 4 | 5 | ```bash 6 | adb install -r ${apk} 7 | ``` 8 | 9 | 清除資料 10 | 11 | ```bash 12 | adb shell pm clear `adb shell pm list packages ${部分名稱}` 13 | ``` 14 | 15 | 移除 app 16 | 17 | ```bash 18 | adb shell pm uninstall `adb shell pm list packages ${部分名稱}` 19 | ``` 20 | 21 | 列出 apps 22 | 23 | ```bash 24 | adb shell pm list packages ${部分名稱} 25 | ``` 26 | 27 | 強制停止 app 28 | 29 | ```bash 30 | adb shell am force-stop `adb shell pm list packages ${部分名稱}` 31 | ``` 32 | 33 | or 34 | 35 | ```bash 36 | adb shell am kill `adb shell pm list packages ${部分名稱}` 37 | ``` 38 | 39 | 喚醒手機 40 | 41 | ```bash 42 | adb shell input keyevent 82 # KeyEvent.KEYCODE_MENU 43 | ``` -------------------------------------------------------------------------------- /akka.md: -------------------------------------------------------------------------------- 1 | # akka 2 | 3 | ## akka + kotlin 4 | 5 | ```kotlin 6 | object start {} 7 | 8 | fun main(args: Array) { 9 | val system = ActorSystem.create(); 10 | val helloActor = system.actorOf(Props(javaClass())); 11 | helloActor.tell(start) 12 | } 13 | 14 | class HelloActor: UntypedActor() { 15 | 16 | var worldActor: ActorRef = getContext().actorOf(Props(javaClass())) 17 | 18 | public override fun onReceive(msg: Any?) { 19 | when (msg) { 20 | start -> worldActor.tell("Hello", self()) 21 | is String-> { 22 | println("Received message: $msg") 23 | getContext().system().shutdown() 24 | } 25 | else -> unhandled(msg) 26 | } 27 | } 28 | 29 | } 30 | 31 | class WorldActor: UntypedActor() { 32 | 33 | public override fun onReceive(msg: Any?) { 34 | when (msg) { 35 | is String -> getSender().tell(msg.toUpperCase() + " world"); 36 | else -> unhandled(msg); 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | ## quasar + kotlin 43 | 44 | ```kotlin 45 | data class Msg(val txt: String, val from: ActorRef) 46 | 47 | class Ping(val n: Int) : Actor() { 48 | Suspendable override fun doRun() { 49 | val pong = ActorRegistry.getActor("pong") 50 | for(i in 1..n) { 51 | pong.send(Msg("ping", self())) // Fiber-blocking 52 | receive { // Fiber-blocking 53 | when (it) { 54 | "pong" -> println("Ping received pong") 55 | else -> null // Discard 56 | } 57 | } 58 | } 59 | pong.send("finished") // Fiber-blocking 60 | println("Ping exiting") 61 | } 62 | } 63 | 64 | class Pong() : Actor() { 65 | Suspendable override fun doRun() { 66 | while (true) { 67 | // snippet Kotlin Actors example 68 | receive(1000, TimeUnit.MILLISECONDS) { // Fiber-blocking 69 | when (it) { 70 | is Msg -> { 71 | if (it.txt == "ping") 72 | it.from.send("pong") // Fiber-blocking 73 | } 74 | "finished" -> { 75 | println("Pong received 'finished', exiting") 76 | return // Non-local return, exit actor 77 | } 78 | is Timeout -> { 79 | println("Pong timeout in 'receive', exiting") 80 | return // Non-local return, exit actor 81 | } 82 | else -> defer() 83 | } 84 | } 85 | // end of snippet 86 | } 87 | } 88 | } 89 | 90 | public class Tests { 91 | Test public fun testActors() { 92 | spawn(register("pong", Pong())) 93 | spawn(Ping(3)) 94 | } 95 | } 96 | ``` 97 | 98 | https://gist.github.com/abreslav/5046126 99 | 100 | ## See Also 101 | 102 | * macroid 103 | * http://blog.paralleluniverse.co/2015/05/21/quasar-vs-akka/ 104 | * http://blog.paralleluniverse.co/2015/06/04/quasar-kotlin/ 105 | -------------------------------------------------------------------------------- /android_sdk.md: -------------------------------------------------------------------------------- 1 | # Android SDK 2 | 3 | 列出套件 4 | 5 | ```bash 6 | android list sdk 7 | ``` 8 | 9 | ```bash 10 | android list sdk --all 11 | ``` 12 | 13 | 安裝 #1 套件 14 | 15 | ```bash 16 | android update sdk -u --filter 1 17 | ``` 18 | 19 | ```bash 20 | android update sdk -u --all --filter #1 21 | ``` 22 | 23 | 移除套件 24 | 25 | ```bash 26 | ... 27 | ``` 28 | 29 | ## ref. 30 | 31 | * http://tools.android.com/recent/updatingsdkfromcommand-line 32 | -------------------------------------------------------------------------------- /android_support_annotations.md: -------------------------------------------------------------------------------- 1 | # Annotations 註記 2 | 3 | 常用的註記介紹. 4 | 5 | 一堆常見的工具庫都有提供註記:google guava, jsr, apache common, google common, android support 6 | 7 | ## NonNull/Nullable 8 | 9 | `@NonNull` 與 `@Nullable` 在一些工具庫也有提供,不過 Android 主流還是使用 Android Support: 10 | 11 | ```java 12 | import android.support.annotation.NonNull; 13 | import android.support.annotation.Nullable; 14 | ... 15 | 16 | /** 17 | * Add support for inflating the tag. 18 | */ 19 | @Nullable 20 | @Override 21 | public View onCreateView(String name, @NonNull Context context, @NonNull AttributeSet attrs) { 22 | ... 23 | ``` 24 | 25 | 這樣的好處是防呆以及省防禦性檢查: 26 | 27 | ```java 28 | public class MyFragment extends Fragment { 29 | @Nullable 30 | @Override 31 | public View onCreateView(String name, @NonNull Context context, @NonNull AttributeSet attrs) { 32 | if (TextUtils.isEmpty(name)) return null; // 這行編譯就會報錯 33 | 34 | if (TextUtils.isEmpty(name)) nam = context.getResources().getString(R.id.app_name); // 不用一一檢查 context != null 35 | // 因為 @NonNull 註記已經確保不讓塞 null 36 | } 37 | } 38 | ``` 39 | 40 | 以及其他: 41 | 42 | ```java 43 | import android.support.annotation.StringRes; 44 | ... 45 | public abstract void setTitle(@StringRes int resId); // 只允許塞 R.string.xxx 46 | ``` 47 | 48 | ```java 49 | import android.support.annotation.IntDef; 50 | ... 51 | public abstract class ActionBar { 52 | ... 53 | @IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS}) 54 | @Retention(RetentionPolicy.SOURCE) 55 | public @interface NavigationMode {} 56 | 57 | public static final int NAVIGATION_MODE_STANDARD = 0; 58 | public static final int NAVIGATION_MODE_LIST = 1; 59 | public static final int NAVIGATION_MODE_TABS = 2; 60 | 61 | @NavigationMode 62 | public abstract int getNavigationMode(); // 不用新增型別 NavigationMode class,直接標記枚舉防呆 63 | 64 | public abstract void setNavigationMode(@NavigationMode int mode); 65 | ``` 66 | 67 | ```java 68 | @IntDef(flag=true, value={ // flag = true 是給 1, 2, 4, 8 等這種旗標性數值使用,所以就算不是枚舉的數字 10 也是合法的,因為 2 + 8 69 | DISPLAY_USE_LOGO, 70 | DISPLAY_SHOW_HOME, 71 | DISPLAY_HOME_AS_UP, 72 | DISPLAY_SHOW_TITLE, 73 | DISPLAY_SHOW_CUSTOM 74 | }) 75 | @Retention(RetentionPolicy.SOURCE) 76 | public @interface DisplayOptions {} 77 | ``` 78 | 79 | ## @VisibleForTesting 80 | 81 | ```java 82 | @VisibleForTesting 83 | public void setLogger(ILogger logger) {...} 84 | ``` 85 | 86 | ## @Keep 87 | 88 | > [@Keep](http://tools.android.com/tech-docs/support-annotations#TOC-Keep) 89 | 90 | > We've also added @Keep to the support annotations. Note however that > that annotation hasn't been hooked up to the Gradle plugin yet (though > it's [in progress](https://android-review.googlesource.com/#/c/152983/).) When finished this will let you annotate methods and > > classes that should be retained when minimizing the app. 91 | 92 | 看起來還在進行中,可以先用筆者的專案:https://github.com/yongjhih/proguard-annotations 93 | 94 | 95 | ## See Also 96 | 97 | * ref. http://tools.android.com/tech-docs/support-annotations 98 | * https://plus.google.com/+StephanLinzner/posts/GBdq6NsRy6S 99 | -------------------------------------------------------------------------------- /annotation_programming.md: -------------------------------------------------------------------------------- 1 | # Annotation Programming 簡介 2 | 3 | meta programming 4 | 5 | * 編譯時期 Processor 6 | * 執行時期 Reflection, InvocationHandler 7 | 8 | 這邊主要討論編譯時期 JSR 269 javax.annotation.processing.AbstractProcessor 9 | 10 | 從常見的 ORM annotations 、json2pojo(Gson, Jackson) 、Dagger1/2、Mockito、Retrofit、AutoValue、AutoParcel、ButterKnife、AndroidAnnotations、RoboGuice 等函式庫,大量利用 annotations 來精簡聚焦、解決煩冗的例行性撰寫程序。 11 | 12 | 像是 [AutoValue](autovalue.md) 就節省了煩冗的 getter 、 setter 、builder 等例行撰寫程序。 13 | 14 | 除了解析 annotations 還包括解析 abstract methods, methods, interfaces, fields 等,緊接著是產生 java file 的 template language 以及物件導向的產生器。 15 | 16 | ## 解析 Annotation 17 | 18 | ```java 19 | @Example 20 | public class ExampleClass implements Runnable { 21 | public final String name; 22 | public ExampleClass(String name) { 23 | this.name = name; 24 | } 25 | @Override 26 | public void run() { 27 | System.out.println(name); 28 | } 29 | } 30 | ``` 31 | 32 | ```java 33 | public ExampleProcessor extends AbstractProcessor { 34 | // ... 35 | @Override 36 | public boolean process(Set annotations, RoundEnvironment env) { 37 | Set elements = env.getElementAnnotatedWith(Example.class); 38 | for (Element e : elements) { // 可列出 @Example 39 | System.out.println(e); 40 | } 41 | return false; 42 | } 43 | } 44 | ``` 45 | 46 | ## 產生器的撰寫方法 47 | 48 | 依據解析結果產生 java file。 49 | 50 | Template Language: 51 | 52 | Apache Velocity Template Language, *.vm 53 | 54 | For AVTL example: 55 | 56 | ```vm 57 | #if (!$pkg.empty) 58 | package $pkg; 59 | #end 60 | 61 | #foreach ($i in $imports) 62 | import $i; 63 | #end 64 | 65 | @${generated}("com.google.auto.value.processor.AutoAnnotationProcessor") 66 | final class $className implements $annotationName { 67 | 68 | ## Fields 69 | 70 | #foreach ($m in $members) 71 | #if ($params.containsKey($m.toString())) 72 | 73 | private final $m.type $m; 74 | 75 | #else 76 | 77 | private static final $m.type $m = $m.defaultValue; 78 | 79 | #end 80 | #end 81 | ``` 82 | 83 | 84 | Square JavaPoet: https://github.com/square/javapoet 85 | 86 | For JavaPoet example: 87 | 88 | ```java 89 | package com.example.helloworld; 90 | 91 | public final class HelloWorld { 92 | public static void main(String[] args) { 93 | System.out.println("Hello, JavaPoet!"); 94 | } 95 | } 96 | ``` 97 | 98 | ```java 99 | MethodSpec main = MethodSpec.methodBuilder("main") 100 | .addModifiers(Modifier.PUBLIC, Modifier.STATIC) 101 | .returns(void.class) 102 | .addParameter(String[].class, "args") 103 | .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!") 104 | .build(); 105 | 106 | TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") 107 | .addModifiers(Modifier.PUBLIC, Modifier.FINAL) 108 | .addMethod(main) 109 | .build(); 110 | 111 | JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld) 112 | .build(); 113 | 114 | javaFile.emit(System.out); 115 | ``` 116 | 117 | ## TypeMirror -> Annoation 118 | 119 | ```java 120 | Annotation annotation = ((DeclaredType) typeMirror).asElement().getAnnotation(AutoValue.Field.class); 121 | ``` 122 | 123 | ## TODO 124 | 125 | https://github.com/yongjhih/JavaPoetic: 126 | 127 | ```java 128 | JavaFile javaFile = JavaFile.package("com.example.helloworld").class( 129 | JavaClass.public().final().name("HelloWorld").method( 130 | JavaMethod.public().static().void().name("main").parameter(String[].class, "args").statement( 131 | JavaStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!"), 132 | JavaStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!") 133 | ) 134 | ) 135 | ) 136 | ); 137 | ``` 138 | 139 | ## 名詞解釋 140 | 141 | APT, Annotation-Processing Tool 142 | 143 | ## bytecode weaving 144 | 145 | * ASM 146 | * AspectJ 147 | * Javasist 148 | 149 | ## See also 150 | 151 | * https://speakerdeck.com/jakewharton/annotation-processing-boilerplate-destruction-square-waterloo-2014 152 | * https://github.com/8tory/simple-parse (runtime) 153 | * https://github.com/8tory/auto-parse (source) 154 | * http://velocity.apache.org/engine/devel/vtl-reference-guide.html 155 | * https://github.com/yongjhih/JavaPoetic 156 | * https://github.com/yongjhih/RetroFacebook 157 | * http://blog.retep.org/2009/02/13/getting-class-values-from-annotations-in-an-annotationprocessor/ 158 | -------------------------------------------------------------------------------- /aspect.md: -------------------------------------------------------------------------------- 1 | # Aspect 2 | 3 | \#AspectJ \#AOP 4 | 5 | AOP 切面導向設計。筆者認為也是一種 Meta Programming 的範疇。 6 | 7 | 我們還是以範例來作說明。在 Android 比較知名的代表作是 [JackWharton/hugo](https://github.com/JakeWharton/hugo) 。 8 | 9 | ```java 10 | @DebugLog 11 | public String getName(String first, String last) { 12 | SystemClock.sleep(15); 13 | return first + " " + last; 14 | } 15 | ``` 16 | 17 | ```java 18 | getName("Andrew", "Chen"); 19 | ``` 20 | 21 | ``` 22 | V/Example: ⇢ getName(first="Andrew", last="Chen") 23 | V/Example: ⇠ getName [16ms] = "Andrew Chen" 24 | ``` 25 | 26 | ```java 27 | @Aspect 28 | public class Hugo { 29 | @Pointcut("within(@hugo.weaving.DebugLog *)") // 產生一個叫 withinAnnotatedClass() 的攔截點,專門攔截有掛 @hugo.weaving.DebugLog 的標記 30 | public void withinAnnotatedClass() {} 31 | 32 | @Pointcut("execution(* *(..)) && withinAnnotatedClass()") 33 | public void methodInsideAnnotatedType() {} // 攔方法參數型別 34 | 35 | @Pointcut("execution(*.new(..)) && withinAnnotatedClass()") 36 | public void constructorInsideAnnotatedType() {} // 攔建構子參數型別 37 | 38 | @Pointcut("execution(@hugo.weaving.DebugLog * *(..)) || methodInsideAnnotatedType()") 39 | public void method() {} // 攔方法 40 | 41 | @Pointcut("execution(@hugo.weaving.DebugLog *.new(..)) || constructorInsideAnnotatedType()") 42 | public void constructor() {} // 攔建構子 43 | 44 | @Around("method() || constructor()") // 如果欄到就 45 | public Object logAndExecute(ProceedingJoinPoint joinPoint) throws Throwable { 46 | long startNanos = System.nanoTime(); 47 | Object result = joinPoint.proceed(); // 代為執行攔截區間 48 | long stopNanos = System.nanoTime(); 49 | // 算一下時間、印一下。 50 | } 51 | } 52 | ``` 53 | 54 | ## See Also 55 | 56 | * https://github.com/JakeWharton/hugo 57 | * https://github.com/orhanobut/tracklytics 58 | * https://github.com/Archinamon/GradleAspectJ-Android 59 | -------------------------------------------------------------------------------- /assert.md: -------------------------------------------------------------------------------- 1 | # Assert 斷言 - assertj, truth 2 | 3 | 斷言: 4 | 5 | * junit 6 | * hamcrest :結合 junit 內建的斷言,提供較好的斷言語義 7 | * truth :仿造 assertj 斷言訊息 8 | * assertj 提供更好懂的斷言訊息 9 | 10 | 其中推薦 assertj 斷言家族 11 | 12 | 測試框架: 13 | 14 | * junit 15 | * testNg :取代 junit 16 | 17 | 一般內建的 junit 即可 18 | 19 | ## assertj-android 20 | 21 | * *註: 筆者 2013 下旬發現* 22 | * *註: 2013 上旬仿 AssertJ 以 fest-android 名稱釋出,2014 中旬正式加入 AssertJ 家族,更名為 assertj-android* 23 | * *註: assertj 2011 釋出* 24 | 25 | 針對 Android 強化斷言錯誤訊息以及語法簡潔。 26 | 27 | 語法的簡潔: 28 | 29 | Before(JUnit): 30 | 31 | ```java 32 | assertEquals(View.GONE, view.getVisibility()); 33 | ``` 34 | 35 | After(AssertJ): 36 | 37 | ```java 38 | assertThat(view).isGone(); 39 | ``` 40 | 41 | 錯誤訊息的強化: 42 | 43 | Before(JUnit): 44 | 45 | ``` 46 | Expected:<[8]> but was:<[4]> 47 | ``` 48 | 49 | After: 50 | 51 | ``` 52 | Expected visibility but was 53 | ``` 54 | 55 | ## truth 56 | 57 | * *註: 筆者是在 2015/2 留意到它* 58 | * *註: 2014/12 由 google testing blog 消息釋出* 59 | * 仿 AssertJ 60 | 61 | 在沒有特定的類別下,提供一個置換錯誤訊息的能力。 62 | 63 | Before(JUnit): 64 | 65 | ```java 66 | boolean buttonEnabled = false; 67 | assertTrue(buttonEnabled); 68 | ``` 69 | 70 | After(truth): 71 | 72 | ```java 73 | ASSERT.that(buttonEnabled).named("buttonEnabled").isTrue(); 74 | ``` 75 | 76 | 錯誤訊息的強化: 77 | 78 | Before(JUnit): 79 | 80 | ``` 81 | was expected be true, but was false 82 | ``` 83 | 84 | After(truth): 85 | 86 | ``` 87 | "buttonEnabled" was expected to be true, but was false 88 | ``` 89 | 90 | ## Hamcrest 91 | 92 | *p.s. 2012 已無動靜* 93 | 94 | 口語化,但無法接龍 95 | 96 | ```java 97 | List list = Arrays.asList("Andrew", "Chen"); 98 | assertThat(list, is(not(empty()))); 99 | assertThat(list, is(contains("Andrew", "Chen"))); 100 | ``` 101 | 102 | ## 小道消息 103 | 104 | Google 的 alexruiz 本來在 2011 年有一個 FEST ([fest-assert](https://github.com/alexruiz/fest-assert-2.x)) 主要是可接龍的斷言,2013 Q2 Square 就以此基礎開發了 fest-assert for android 叫做 fest-android 105 | ,但是後來 2014 Q2 改用 AssertJ Core 為基礎,改名 assertj-android 成為 AssertJ 家族成員。 106 | 107 | fest-assert 的第二作者 joel-costigliola 2011 就分家 AssertJ 了,而 fest-assert 看起來 2013 Q2 都沒有什麼動靜了,直到 2014 Q4 Google 另外釋出 [truth](https://github.com/google/truth) ,也是可接龍的斷言,不過似乎也沒有太多明顯優勢。 108 | 109 | 以生態來說,仍以 AssertJ 為大宗 110 | 111 | ## See Also 112 | 113 | * http://developer.android.com/training/testing/unit-testing/local-unit-tests.html 114 | * https://github.com/square/assertj-android 115 | * https://github.com/google/truth 116 | * http://joel-costigliola.github.io/assertj 117 | * https://github.com/google/truth/issues/43 118 | * http://googletesting.blogspot.tw/2014/12/testing-on-toilet-truth-fluent.html 119 | * https://github.com/ribot/assertj-rx 120 | * https://github.com/ubiratansoares/rxassertions 121 | * https://github.com/hamcrest/JavaHamcrest 122 | * http://google.github.io/truth/comparison 123 | -------------------------------------------------------------------------------- /authors.md: -------------------------------------------------------------------------------- 1 | # 作者群介紹 2 | 3 | 這本書本來目標就是共筆書籍,所以有第 N 作者。 4 | 5 | ## 第一作者 6 | 7 | yongjhih, Andrew Chen 8 | 9 | ![yongjhih](https://avatars3.githubusercontent.com/u/213736?v=3&s=160) 10 | 11 | > 為什麼你會寫本書? 12 | 13 | 其實因緣際會,本來只想寫一篇而已。 14 | 15 | 一開始主要工作上的需要,採用了 RxJava 的函式庫,為了讓夥伴們可以上手,但是它的「上手」文章略嫌不足,所以才開始撰寫了第一個章節 RxJava ,接著想說順便把之前的一些開發經驗讓夥伴們可以瞭解,陸陸續續的就越寫越多章節,就變成這樣了。 16 | 17 | > 你是哪時候開始接觸 Android App 開發? 18 | 19 | 嚴格講起來是從 2013 年,九月份左右才開始。到現在 2015 年約兩年多。 20 | 21 | 因為其實一開始是在手機系統廠工作,做了三年,從 2010 年中開始的到 2013 年中。那時也雖然也有改 Android App 不過就是 AOSP 系統內建的那些 App 稍微修修 Bug 調整一下 UI 之類的。獨立開發 App 倒還不曾有過。App 實際開發起來,是截然不同的。 22 | 23 | > 你現在在哪工作? 24 | 25 | 新創公司幫人寫寫 App 以及建立開發環境與佈署環境。 26 | 27 | 2013, SCM, ITS, Code Review, CI / gitlab, phabricator, jenkins. 內部系統都放置於 docker container. 28 | 29 | 2014, Backend Couchabase. Analytics System(with docker): Webdis, Redis, Logstash, ElasticSearch, Kibana. 30 | 31 | 2015, Parse. 32 | 33 | > Github? 34 | 35 | https://github.com/yongjhih 36 | 37 | > LinkedIn? 38 | 39 | https://linkedin.com/in/yongjhih 40 | 41 | ## 第二作者 42 | 43 | 負責章節: 44 | 45 | 46 | 47 | ## 編輯 48 | -------------------------------------------------------------------------------- /autovalue.md: -------------------------------------------------------------------------------- 1 | # AutoValue 2 | 3 | p.s. *其他類似的專案 [Immutables](https://github.com/immutables/immutables) (誰抄誰就不得而知了)* 4 | 5 | 一個透過 Annotation Programming 簡化撰寫 identity model 的編譯時期函式庫。 6 | 7 | Before: 8 | 9 | ```java 10 | public class User { 11 | public String name; 12 | public int id; 13 | 14 | public String name() { 15 | return name; 16 | } 17 | 18 | public int id() { 19 | return id; 20 | } 21 | 22 | public User(String name, int id) { 23 | this.name = name; 24 | this.id = id; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return "User{" 30 | + "name=" + name 31 | + ", id=" + id 32 | + "}"; 33 | } 34 | 35 | @Override 36 | public boolean equals(Object o) { 37 | if (o == this) { 38 | return true; 39 | } 40 | if (o instanceof User) { 41 | User that = (User) o; 42 | return (this.name.equals(that.name())) 43 | && (this.id == that.id()); 44 | } 45 | return false; 46 | } 47 | 48 | @Override int hashCode() { 49 | return Objects.hashCode(name, id); 50 | } 51 | } 52 | ``` 53 | 54 | ```java 55 | User andrew = new User("Andrew", 1); 56 | User andrew1 = new User("Andrew", 1); 57 | User andrew2 = new User("Andrew", 2); 58 | System.out.println(andrew.equals(andrew1)); 59 | System.out.println(andrew.equals(andrew2)); 60 | ``` 61 | 62 | After: 63 | 64 | ```java 65 | import com.google.auto.value.AutoValue; 66 | 67 | @AutoValue 68 | public abstract class User { 69 | public abstract String name(); 70 | public abstract int id(); 71 | 72 | public static User builder() { 73 | return new AutoValue_User.Builder(); 74 | } 75 | 76 | @AutoValue.Builder 77 | interface Builder { 78 | Builder name(String s); 79 | Builder id(int n); 80 | User build(); 81 | } 82 | } 83 | ``` 84 | 85 | ```java 86 | User andrew = User.builder().name("Andrew").id(1).build(); 87 | User andrew1 = User.builder().name("Andrew").id(1).build(); 88 | User andrew2 = User.builder().name("Andrew").id(2).build(); 89 | System.out.println(andrew.equals(andrew1)); 90 | System.out.println(andrew.equals(andrew2)); 91 | ``` 92 | 93 | ## Android Parcelable 94 | 95 | [frankiesardo/auto-parcel](https://github.com/frankiesardo/auto-parcel): 96 | 97 | ```java 98 | @AutoParcel 99 | abstract class SomeModel implements Parcelable { 100 | abstract String name(); 101 | abstract List subModels(); 102 | abstract Map modelsMap(); 103 | 104 | public static Builder builder() { 105 | return new AutoParcel_SomeModel.Builder(); 106 | } 107 | 108 | @AutoParcel.Builder 109 | public interface Builder { 110 | public Builder name(String x); 111 | public Builder subModels(List x); 112 | public Builder modelsMap(Map x); 113 | public SomeModel build(); 114 | } 115 | } 116 | ``` 117 | 118 | 或者 https://github.com/johncarl81/parceler 119 | 120 | ```java 121 | @AutoValue 122 | @Parcel 123 | public abstract class AutoValueParcel { 124 | 125 | @ParcelProperty("value") public abstract String value(); 126 | 127 | @ParcelFactory 128 | public static AutoValueParcel create(String value) { 129 | return new AutoValue_AutoValueParcel(value); 130 | } 131 | } 132 | ``` 133 | 134 | parceler 的產生器, 有點獨特.. 135 | 136 | ## See Also 137 | 138 | * https://github.com/vbauer/jackdaw 139 | * https://github.com/frankiesardo/auto-parcel 140 | * https://github.com/johncarl81/parceler 141 | * https://github.com/rharter/auto-value-parcel 142 | * https://github.com/immutables/immutables 143 | -------------------------------------------------------------------------------- /bolts-android.md: -------------------------------------------------------------------------------- 1 | # Bolts-Android 2 | 3 | 如果你已經在用 RxJava ,那這應該僅供參考。 4 | 5 | Bolts 是一款 promise 的實現。由 Parse.com 主持。Facebook 在收購 Parse.com 後,也整合 bolts 了。 6 | 7 | `Bolts.Task` 相當於`Observable` 8 | 9 | `Bolts.Task.continueWith()` 相當於 `Observable.map()` 10 | 11 | ```java 12 | Task saveAsync; 13 | ... 14 | // .map(o -> null); 15 | saveAsync(obj).continueWith(new Continuation() { 16 | public Void then(Task task) throws Exception { 17 | if (task.isCancelled()) { 18 | // 取消 19 | } else if (task.isFaulted()) { 20 | // 失敗 21 | Exception error = task.getError(); 22 | } else { 23 | // 成功 24 | ParseObject object = task.getResult(); 25 | } 26 | return null; 27 | } 28 | }); 29 | ``` 30 | 31 | `Bolts.Task.continueWithTask()` 相當於 `Observable.flatMap()` 32 | 33 | ```java 34 | // .flatMap(o -> saveObs(o)).map(o -> null); 35 | query.findInBackground().continueWithTask(new Continuation, Task>() { 36 | public Task then(Task> task) throws Exception { 37 | if (task.isFaulted()) { 38 | return null; 39 | } 40 | 41 | List students = task.getResult(); 42 | students.get(1).put("salutatorian", true); 43 | return saveAsync(students.get(1)); 44 | } 45 | }).onSuccess(new Continuation() { 46 | public Void then(Task task) throws Exception { 47 | return null; 48 | } 49 | }); 50 | ``` 51 | 52 | `Task.forResult()` 相當於 `Observable.just()` 53 | 54 | ```java 55 | Task successful = Task.forResult("The good result."); 56 | ``` 57 | 58 | `Task.create()` 相當於 `Observable.create()` 59 | 60 | ```java 61 | public Task fetchAsync(ParseObject obj) { 62 | final Task.TaskCompletionSource tcs = Task.create(); 63 | obj.fetchInBackground(new GetCallback() { 64 | public void done(ParseObject object, ParseException e) { 65 | if (e == null) { 66 | tcs.setResult(object); 67 | } else { 68 | tcs.setError(e); 69 | } 70 | } 71 | }); 72 | return tcs.getTask(); 73 | } 74 | ``` 75 | 76 | `Task.callInBackground()` 相當於 `Observable.defer()`/`Observable.fromCallbable()`: 77 | 78 | ```java 79 | Task Task.callInBackground(new Callable() { 80 | public Void call() { 81 | // Do a bunch of stuff. 82 | } 83 | }); 84 | ``` 85 | 86 | `Task.waitForCompletion()` 相當於 `Observable.toBlocking()`: 87 | 88 | Tasks.java: 89 | 90 | ```java 91 | public static T wait(Task task) { 92 | try { 93 | task.waitForCompletion(); 94 | if (task.isFaulted()) { 95 | Exception error = task.getError(); 96 | if (error instanceof RuntimeException) { 97 | throw (RuntimeException) error; 98 | } 99 | throw new RuntimeException(error); 100 | } else if (task.isCancelled()) { 101 | throw new RuntimeException(new CancellationException()); 102 | } 103 | return task.getResult(); 104 | } catch (InterruptedException e) { 105 | throw new RuntimeException(e); 106 | } 107 | } 108 | ``` 109 | 110 | ## RxBolts - Task2Observable 111 | 112 | Task 轉 Observable 113 | 114 | 範例: 115 | 116 | ```java 117 | TaskObservable.defer(() -> Task.forResult("Hello, world!")).subscribe(it -> { 118 | System.out.println(it); 119 | }); 120 | ``` 121 | 122 | 實現: 123 | 124 | ref. [yongjhih/RxBolts/.../TaskObservable.java](https://github.com/yongjhih/RxBolts/blob/master/rxbolts/src/main/java/rx/bolts/TaskObservable.java#L36) 125 | 126 | ```java 127 | public static Observable defer(Task task) { 128 | return Observable.create(sub -> { 129 | task.continueWith(t -> { 130 | if (t.isCancelled()) { 131 | sub.unsubscribe(); //sub.onCompleted();? 132 | } else if (t.isFaulted()) { 133 | sub.onError(t.getError()); 134 | } else { 135 | R r = t.getResult(); 136 | if (r != null) sub.onNext(r); 137 | sub.onCompleted(); 138 | } 139 | return null; 140 | }); 141 | }); 142 | } 143 | ``` 144 | 145 | ## See Also 146 | 147 | * https://github.com/BoltsFramework/Bolts-Android 148 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "disqus", 4 | "ga", 5 | "gtoc", 6 | "edit-link", 7 | "anchors" 8 | ], 9 | "pluginsConfig": { 10 | "disqus": { 11 | "shortName": "yongjhih-gitbook" 12 | }, 13 | "ga": { 14 | "token": "UA-60405492-1" 15 | }, 16 | "edit-link": { 17 | "base": "https://github.com/yongjhih/feed/edit/master", 18 | "label": "Edit This Page" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /clean_architecture.md: -------------------------------------------------------------------------------- 1 | # Clean architecture 2 | 3 | 先將程式碼擺放包裝劃分清楚再來針對動線做規劃。 4 | 5 | Clean architecture 主要探討的是系統分層以及互動模型 6 | 7 | 常見的互動模型: 8 | 9 | * MVC 10 | * MVP (P, Presenter) 11 | * MVVM (VM, ViewModel) 12 | * Flux 13 | 14 | 18 | 19 | 在 Android 開發上,常見的是 MVP 與 MVVM 一般開發上,並不一定都只有一套互動模型在運行。 20 | 21 | MVP 其實大家應該十分熟悉,只是沒有意識到而已。那就是 ListView/RecyclerView 就有用到了。 22 | 23 | 我們知道 MVP 三層,所以只要介紹中間那層 P 會比較直接,這邊稍微簡化舉例: 24 | 25 | ```java 26 | class UserCardPresenter extends Presenter { 27 | public ViewHolder onCreateViewHolder(ViewGroup parent) { 28 | return new UserCardViewHolder(parent, R.layout.item_icon); 29 | } 30 | 31 | public void onBindViewHolder(/* View */UserCardViewHolder userCardView, /* Model */ User user) { 32 | // userCardView.textView1.setText(item.name); 33 | } 34 | } 35 | ``` 36 | 37 | MVVM 基本上,可當作是 MVP 的子集合,注重雙向連動,以利個別測試,你可以虛擬化其中一方做測試。 38 | 39 | 40 | ## ogaclejapan/RxBinding MVVM 41 | 42 | 利用 RxJava 實現了 MVVM 的 two-way binding (雙向連動) 43 | 44 | ## Data-Binding support v22 45 | 46 | 讓連動更簡便撰寫 VM 應該就更薄了。 47 | 48 | ## Flux 49 | 50 | ![](https://facebook.github.io/flux/img/flux-simple-f8-diagram-with-client-action-1300w.png) 51 | 52 | 不直接雙向回 model/store ,透過一個 dispatcher 來做管理。 53 | 54 | ## See Also 55 | 56 | * http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/ 57 | * http://fernandocejas.com/2015/07/18/architecting-android-the-evolution/ 58 | * http://manuel.kiessling.net/2012/09/28/applying-the-clean-architecture-to-go-applications/ 59 | * https://www.groupbuddies.com/posts/20-clean-architecture 60 | * http://konmik.github.io/introduction-to-model-view-presenter-on-android.html 61 | * https://medium.com/mobiwise-blog/android-basic-project-architecture-for-mvp-72f4b33252d0 62 | * http://armueller.github.io/android/2015/03/29/flux-and-android.html 63 | * http://lgvalle.xyz/2015/08/04/flux-architecture/ 64 | 65 | * https://github.com/skimarxall/RxFlux 66 | 67 | Sample: 68 | 69 | * https://github.com/android10/Android-CleanArchitecture 70 | * https://github.com/sockeqwe/mosby 71 | * https://github.com/JorgeCastilloPrz/Dagger2Scopes 72 | * https://github.com/PaNaVTEC/Clean-Contacts 73 | * https://github.com/techery/presenta 74 | * https://github.com/antoniolg/androidmvp 75 | * https://github.com/wongcain/MVP-Simple-Demo 76 | * https://github.com/pedrovgs/EffectiveAndroidUI 77 | * https://github.com/glomadrian/MvpCleanArchitecture 78 | * https://github.com/spengilley/ActivityFragmentMVP 79 | * https://github.com/jlmd/UpcomingMoviesMVP 80 | * https://github.com/JorgeCastilloPrz/EasyMVP 81 | * https://github.com/richardradics/MVPAndroidBootstrap 82 | * https://github.com/richardradics/RxAndroidBootstrap 83 | * https://github.com/inloop/AndroidViewModel 84 | * https://github.com/lgvalle/android-flux-todo-app 85 | -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongjhih/android-gitbook/53c322818684527c40f46c82fcdcaefbd2d76243/cover.jpg -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongjhih/android-gitbook/53c322818684527c40f46c82fcdcaefbd2d76243/cover.png -------------------------------------------------------------------------------- /dagger2.md: -------------------------------------------------------------------------------- 1 | # Dagger2 2 | 3 | DI 工具 4 | 5 | ## 什麼是 DI 6 | 7 | DI, Dependency Injection (相依性注入) ,Anti-DIY, Auto-DIY 自動組裝,行前準備/著裝/組件/要件 8 | 9 | 例如 ButterKnife 就是一種 DI ,只是針對 findView 來做的 Injection 。 10 | 11 | 在特定的生命週期,幫你生成所需的物件。Dagger 是讓你創造自己的 Injection。 12 | 13 | 另外一個重點是,重用元件。 14 | 15 | ## 著名的咖啡機 16 | 17 | ![CoffeeMaker](http://upload.wikimedia.org/wikipedia/commons/2/23/KBG741S-AO.jpg) 18 | 19 | 沖泡出一杯風味十足的咖啡之前,你需要一台濾泡式咖啡機。 20 | 21 | 需要的元件有: 22 | 23 | * 加熱器:把水加熱 24 | * 幫浦 25 | 26 | 在沒有的 DI 概念下: 27 | 28 | Before: 29 | 30 | ```java 31 | CoffeMaker maker = new CoffeeMaker(); 32 | maker.brew(); // 沖泡 33 | maker.brew(); // 沖泡 34 | ``` 35 | 36 | ```java 37 | class CoffeeMaker { // 咖啡機 38 | private final Heater heater; // 加熱器 39 | private final Pump pump; // 幫浦 40 | 41 | CoffeeMaker() { 42 | this.heater = new ElectricHeater(); // 電熱器 43 | this.pump = new Thermosiphon(heater); // 虹吸幫浦(虹吸,剛好也需要加熱器) 44 | } 45 | 46 | public void brew() { /* ... */ } 47 | } 48 | ``` 49 | 50 | 缺點是每杯咖啡生產的整台咖啡機、電熱器與幫浦,這些組件都無法延用到其他裝置身上,太浪費了,一點都不環保。 51 | 52 | 手動 DI: 53 | 54 | After: 55 | 56 | ```java 57 | Heater heater = new ElectricHeater(); 58 | Pump pump = new Thermosiphon(heater); 59 | CoffeeMaker maker = new CoffeeMaker(heater, pump); 60 | CoffeeMaker maker2 = new CoffeeMaker(heater, pump); 61 | maker.brew(); 62 | maker2.brew(); 63 | ``` 64 | 65 | ```java 66 | class CoffeeMaker { 67 | private final Heater heater; 68 | private final Pump pump; 69 | 70 | CoffeeMaker(Heater heater, Pump pump) { 71 | this.heater = heater; 72 | this.pump = pump; 73 | } 74 | 75 | public void brew() { /* ... */ } 76 | } 77 | ``` 78 | 79 | 這樣至少電熱器與幫浦都可以有機會重覆拿給別人使用了。 80 | 81 | 但是這樣我們要泡咖啡前,都要自己準備電熱器與幫浦然後組裝成咖啡機後才開始泡咖啡。 82 | 83 | 我們希望寫好咖啡機所需要的組件,請一個組裝工人幫我們組 84 | ,以後只要說我現在要用咖啡機就馬上組裝好了,我們都不用自己 DIY。其他裝置也是由這個工人負責組裝,這個工人可以沿用相同零組件來生產。 85 | 86 | 利用 Dagger2 自動 DI 來組裝那些要件,只要描述好要件相依後,就可以一直泡一直泡: 87 | 88 | ```java 89 | Coffee coffee = Dagger_CoffeeApp_Coffee.create(); 90 | Coffee coffee2 = Dagger_CoffeeApp_Coffee.create(); 91 | coffee.maker().brew(); // 一直泡 92 | coffee2.maker().brew(); // 一直泡 93 | ``` 94 | 95 | 咖啡機要加熱加壓沖泡,相依要件關係圖: 96 | 97 | ``` 98 | CoffeeMaker -> DripCoffeeModule -----------------------> Heater 99 | \-> PumpModule -> ThermosiphonPump -/ 100 | ``` 101 | 102 | 濾泡式咖啡機需要把水加熱、加壓後沖泡: 103 | 104 | ```java 105 | class CoffeeMaker { 106 | private final Lazy heater; // 加熱器 107 | private final Pump pump; 108 | 109 | // 準備幫浦與加熱器 110 | @Inject CoffeeMaker(Lazy heater, Pump pump) { 111 | this.heater = heater; 112 | this.pump = pump; 113 | } 114 | 115 | // 沖泡 116 | public void brew() { 117 | heater.get().on(); // 加熱 118 | pump.pump(); // 加壓 119 | System.out.println(" [_]P coffee! [_]P "); // 熱騰騰的咖啡出爐囉! 120 | heater.get().off(); // 隨手關加熱器 121 | } 122 | } 123 | ``` 124 | 125 | 開始寫組裝說明書: 126 | 127 | ```java 128 | @Singleton // 共用咖啡機 129 | @Component(modules = DripCoffeeModule.class) // 濾泡裝置(安裝著高壓熱水沖泡裝置提供幫浦與加熱器) 130 | public interface Coffee { 131 | CoffeeMaker maker(); 132 | } 133 | ``` 134 | 135 | 濾泡裝置需要幫浦加壓器具: 136 | 137 | ```java 138 | @Module(includes = PumpModule.class) // 一同準備加壓器具 139 | class DripCoffeeModule { // 濾泡裝置 140 | @Provides @Singleton Heater provideHeater() { // 提供共用的加熱器具 141 | return new ElectricHeater(); // 電熱器具 142 | } 143 | } 144 | ``` 145 | 146 | 幫浦: 147 | 148 | ```java 149 | @Module(complete = false, library = true) // complete = false 需要借用加熱器具 150 | class PumpModule { // 幫浦加壓器具 151 | @Provides Pump providePump(Thermosiphon pump) { // 利用熱虹吸管來提供幫浦能力 152 | return pump; 153 | } 154 | } 155 | ``` 156 | 157 | 熱虹吸管幫浦: 158 | 159 | ```java 160 | class Thermosiphon implements Pump { 161 | private final Heater heater; 162 | 163 | @Inject 164 | Thermosiphon(Heater heater) { // 索取加熱器來加壓 165 | this.heater = heater; 166 | } 167 | 168 | @Override public void pump() { 169 | if (heater.isHot()) { 170 | System.out.println("=> => pumping => =>"); 171 | } 172 | } 173 | } 174 | ``` 175 | 176 | ## 動手玩 177 | 178 | ```bash 179 | git clone https://github.com/yongjhih/dagger2-sample 180 | cd dagger2-sample 181 | ./gradlew execute 182 | ``` 183 | 184 | ## 範例 1 185 | 186 | Before: 187 | 188 | ```java 189 | OkHttpClient client = new OkHttpClient(); 190 | TwitterApi api = new TwitterApi(client); 191 | 192 | String user = "Andrew Chen"; 193 | 194 | Tweeter tweeter = new Tweeter(api, user); 195 | tweeter.tweet("Hello, world!"); 196 | 197 | Timeline timeline = new Timeline(api, user); 198 | for (Tweet tweet : timeline.get()) { 199 | System.out.println(tweet); 200 | } 201 | 202 | String user2 = "Andrew Chen2"; 203 | 204 | Tweeter tweeter2 = new Tweeter(api, user2); 205 | tweeter.tweet("Hello, world!"); 206 | 207 | Timeline timeline2 = new Timeline(api, user2); 208 | for (Tweet tweet : timeline.get()) { 209 | System.out.println(tweet); 210 | } 211 | ``` 212 | 213 | 隱藏相依前置作業,透過 builder 來獲得末端物件。 214 | 215 | After: 216 | 217 | ```java 218 | ApiComponent apiComponent = Dagger_ApiComponent.create(); 219 | 220 | TwitterComponent twitterComponent = Dagger_TwitterComponent.builder() 221 | .apiComponent(apiComponent) 222 | .twitterModule(new TwitterModule("Andrew Chen")) 223 | .build(); 224 | 225 | Tweeter tweeter = twitterComponent.tweeter(); 226 | Timeliner timeline = twitterComponent.timeline(); 227 | 228 | for (Tweet tweet : timeline.get()) { 229 | System.out.println(tweet); 230 | } 231 | 232 | TwitterComponent component2 = Dagger_TwitterComponent.builder() 233 | .apiComponent(apiComponent) 234 | .twitterModule(new TwitterModule("Andrew Chen2")) 235 | .build(); 236 | 237 | Tweeter tweeter2 = component2.tweeter(); 238 | Timeliner timeline2 = component2.timeline(); 239 | 240 | for (Tweet tweet : timeline.get()) { 241 | System.out.println(tweet); 242 | } 243 | ``` 244 | 245 | 連帶修改: 246 | 247 | ```java 248 | @Module 249 | public class NetworkModule { 250 | @Provides @Singleton // @Singleto 註明沿用同一個, @Provides 註明可提供 OkHttpClient 251 | OkHttpClient provideOkHttpClient() { 252 | return new OkHttpClient(); 253 | } 254 | } 255 | ``` 256 | 257 | ```java 258 | @Singleton 259 | public class TwitterApi { 260 | private final OkHttpClient client; 261 | 262 | @Inject // 註明由相依提供 OkHttpClient 263 | public TwitterApi(OkHttpClient client) { 264 | this.client = client; 265 | } 266 | } 267 | ``` 268 | 269 | ```java 270 | @Singleton 271 | @Component(modules = NetworkModule.class) // 由哪些 modules 組成 272 | public interface ApiComponent { 273 | TwitterApi api(); 274 | } 275 | ``` 276 | 277 | ```java 278 | @Module 279 | public class TwitterModule { 280 | private final String user; 281 | 282 | public TwitterModule(String user) { 283 | this.user = user; 284 | } 285 | 286 | @Provides 287 | Tweeter provideTweeter(TwitterApi api) { 288 | return new Tweeter(api, user); 289 | } 290 | 291 | @Provides 292 | Timeline provideTimeline(TweeterApi api) { 293 | return new Timeline(api, user); 294 | } 295 | } 296 | ``` 297 | 298 | ```java 299 | @Component( 300 | dependencies = ApiComponent.class, 301 | modules = TwitterModule.class 302 | ) 303 | public interface TwitterComponent { 304 | Tweeter tweeter(); 305 | Timeline timeline(); 306 | } 307 | ``` 308 | 309 | ## 範例 2 - OkHttpClient 310 | 311 | ## 範例 3 - Facebook 312 | 313 | ## 範例 4 - AndroidUniversalImageLoader 314 | ## 範例 5 - Fresco 315 | ## 範例 5 - Picasso 316 | ## 範例 5 - Parse 317 | 318 | ## See Also 319 | 320 | * https://speakerdeck.com/jakewharton/dependency-injection-with-dagger-2-devoxx-2014 321 | * http://google.github.io/dagger/ 322 | * https://www.youtube.com/watch?v=oK_XtfXPkqw 323 | * https://github.com/JorgeCastilloPrz/Dagger2Scopes 324 | * http://fernandocejas.com/2015/04/11/tasting-dagger-2-on-android/ 325 | -------------------------------------------------------------------------------- /dagger2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongjhih/android-gitbook/53c322818684527c40f46c82fcdcaefbd2d76243/dagger2.png -------------------------------------------------------------------------------- /debug_stetho_,_leakcanary.md: -------------------------------------------------------------------------------- 1 | # Debug 除錯 - stetho, leakcanary 2 | 3 | ## facebook/stetho 4 | 5 | ## square/leakcanary -------------------------------------------------------------------------------- /dev.md: -------------------------------------------------------------------------------- 1 | # 開發環境 2 | 3 | ## 編譯 4 | 5 | ```sh 6 | $ curl -L https://github.com/yongjhih/docker-android/raw/master/docker-android > ~/bin/docker-android && \ 7 | chmod a+x ~/bin/docker-android 8 | 9 | $ docker-android ./gradlew assembleDebug 10 | ``` 11 | 12 | ## IDE: Android Studio 13 | 14 | ```sh 15 | $ curl -L https://github.com/yongjhih/docker-android-studio/raw/master/docker-android-studio > ~/bin/android-studio && \ 16 | chmod a+x ~/bin/android-studio 17 | 18 | $ android-studio 19 | ``` 20 | -------------------------------------------------------------------------------- /docker.md: -------------------------------------------------------------------------------- 1 | # Docker 2 | 3 | ## 常用指令 4 | 5 | 開放使用者權限 6 | 7 | ```bash 8 | sudo usermod -aG docker andrew 9 | ``` 10 | 11 | 啟動 imag: 12 | 13 | ```bash 14 | docker run -it ubuntu /bin/bash 15 | ``` 16 | 17 | 指定 image 版本啟動 (-i interactive, -t tty): 18 | 19 | ```bash 20 | docker run -it ubuntu:14.04 /bin/bash 21 | ``` 22 | 23 | 背景啟動 image (-d dettach): 24 | 25 | ``` 26 | docker run -d ubuntu 27 | ``` 28 | 29 | 進入執行中的 container: 30 | 31 | ```bash 32 | docker exec -it c007a10e4 bash 33 | ``` 34 | 35 | 更新 image: 36 | 37 | ```bash 38 | docker pull ubuntu 39 | ``` 40 | 41 | 停止執行中的 container: 42 | 43 | ```bash 44 | docker stop c007a10e4 45 | ``` 46 | 47 | 啟動停止執行中的 container: 48 | 49 | ```bash 50 | docker start c007a10e4 51 | ``` 52 | 53 | 列出執行中的 containers: 54 | 55 | ```bash 56 | docker ps 57 | ``` 58 | 59 | 列出包括停止的 containers (-a all): 60 | 61 | ```bash 62 | docker ps -a 63 | ``` 64 | 65 | 列出下載的 images: 66 | 67 | ```bash 68 | docker images 69 | ``` 70 | 71 | 掛目錄進去 (-v volume): 72 | 73 | ```java 74 | docker run -d ubuntu -v /home:/var/home 75 | ```` 76 | 77 | 78 | 設定 port (-p port): 79 | 80 | ```java 81 | docker run -d ubuntu -p 80:3000 82 | ```` 83 | 84 | 設定環境變數 (-e env): 85 | 86 | ```java 87 | docker run -d ubuntu -e "http_proxy=http://192.168.1.254:3128" 88 | ```` 89 | 90 | ## docker-gen 91 | 92 | 透過 container 資訊生成設定檔 93 | 94 | 以前透過 `docker inspect 6680cc9d6d9a` 取得資訊參考來寫設定檔,現在透過樣板語言來生成。 95 | 96 | 例如: 97 | 98 | ```sh 99 | docker-gen nginx.tmpl nginx.conf 100 | ``` 101 | 102 | 延伸用法產生設定檔後自動重啟指定 container : 103 | 104 | ```sh 105 | docker-gen -notify-sighup nginx -watch -only-exposed -wait 5s:30s nginx.tmpl nginx.conf 106 | ``` 107 | 108 | 案例 - letencrypt 自動生成 nginx 與 proxy: 109 | 110 | ```yml 111 | simple-site: 112 | image: nginx 113 | container_name: simple-site 114 | ports: 115 | - "8080:80" 116 | volumes: 117 | - "./volumes/examples/simple-site/conf.d/:/etc/nginx/conf.d" 118 | environment: 119 | - VIRTUAL_HOST=site.example.com 120 | - LETSENCRYPT_HOST=site.example.com 121 | - LETSENCRYPT_EMAIL=email@example.com 122 | 123 | nginx: 124 | image: nginx 125 | container_name: nginx 126 | ports: 127 | - "80:80" 128 | - "443:443" 129 | volumes: 130 | - "/etc/nginx/conf.d" 131 | - "/etc/nginx/vhost.d" 132 | - "/usr/share/nginx/html" 133 | - "./volumes/proxy/certs:/etc/nginx/certs:ro" 134 | nginx-gen: 135 | image: jwilder/docker-gen 136 | container_name: nginx-gen 137 | volumes: 138 | - "/var/run/docker.sock:/tmp/docker.sock:ro" 139 | - "./volumes/proxy/templates/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro" 140 | volumes_from: 141 | - nginx 142 | entrypoint: /usr/local/bin/docker-gen -notify-sighup nginx -watch -only-exposed -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf 143 | letsencrypt-nginx-proxy-companion: 144 | image: jrcs/letsencrypt-nginx-proxy-companion 145 | container_name: letsencrypt-nginx-proxy-companion 146 | volumes_from: 147 | - nginx 148 | volumes: 149 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 150 | - "./volumes/proxy/certs:/etc/nginx/certs:rw" 151 | environment: 152 | - NGINX_DOCKER_GEN_CONTAINER=nginx-gen 153 | ``` 154 | 155 | * jrcs/letsencrypt-nginx-proxy-companion 啟動後會產生憑證 156 | * nginx 等待生成設定檔與憑證產生 157 | * nginx-gen 等待憑證產生,並依據 simple-site 接著產生 proxy 設定檔,重啟 nginx 生效 158 | 159 | 使用者只要維護 simple-site 的部份就好,就會自動產生憑證,且 proxy 也會幫你設定好。 160 | 161 | 共享 certs 憑證目錄 162 | 163 | ## 編註 164 | 165 | * 這是一篇與 Android 開發較微無關的章節。屬於後端平台性的章節。 166 | * 筆者是在 2013/11, 0.6.7 之後的版本接觸。 167 | -------------------------------------------------------------------------------- /effective_android.md: -------------------------------------------------------------------------------- 1 | # Android 的日常 2 | 3 | 這邊主要抽一些日常的小撇步。 4 | 5 | ## 不要用 Thread 或 HandlerThread 推薦使用 AsyncTask 6 | 7 | 如果要背景下載檔案,應該使用 IntentService 而不是自己去操作 Thread。 8 | 9 | 如果是這個 Activity 內的長時間存取,可以使用 AsyncTask 。如有必要請搭配 LoaderManager 使用。 10 | 11 | ## 不要用 Handle Message 推薦使用 `post(Runnable)` 12 | 13 | 通常你其實需要的是 `post(Runnable)` 14 | 15 | ```java 16 | post(() -> updateProgress()); 17 | ``` 18 | 19 | 這樣不用再管理編號了。 20 | 21 | ```java 22 | static final int MSG_UPDATE_PROGRESS = 0; 23 | 24 | // ... 25 | 26 | MSG_UPDATE_PROGRESS: 27 | updateProgress(); 28 | break; 29 | 30 | // ... 31 | ``` 32 | 33 | ## 推薦使用 `postDelayed(milliseconds, Runnable)` 來做延遲呼叫 34 | 35 | ## 防彈跳推薦 `removeCallback(Runnable)` + `postDelayed(Runnable)` 不再量測時間 timeout 36 | 37 | ```java 38 | removeCallback(updateProgressRunnable); 39 | postDelayed(300, updateProgressRunnable); 40 | ``` 41 | 42 | ## 不再 findViewById 推薦使用 ButterKnife 43 | 44 | After: 45 | 46 | ```java 47 | @BindView(R.id.username) 48 | TextView usernameView; 49 | 50 | 51 | // ... 52 | 53 | @Override public void onCreate(Bundle savedInstanceState) { 54 | // ... 55 | ButterKnife.bind(this); 56 | } 57 | ``` 58 | 59 | Before: 60 | 61 | ```java 62 | TextView usernameView; 63 | 64 | @Override public void onCreate(Bundle savedInstanceState) { 65 | // ... 66 | usernameView = (TextView) findViewById(R.id.username); 67 | } 68 | ``` 69 | 70 | ## 使用 @Nullable/@NonNull 標注 71 | 72 | 以及各種 support-annotations 73 | 74 | * @IntDef 75 | * @Keep 76 | * etc. 77 | 78 | ## 宣告型別應盡可能抽象型別,應 `List` 非 `ArrayList` 79 | 80 | * `Map` 非 `HashMap` 81 | 82 | ```java 83 | List getNames(List users) { 84 | List names = new ArrayList<>(); 85 | for (User user : users) names.add(user.name()); 86 | return names; 87 | } 88 | ``` 89 | 90 | ## 使用泛型建置技巧 `new ArrayList<>()` 91 | 92 | java7 之後可省略型別,直接推定型別 93 | 94 | ```java 95 | List names = new ArrayList<>(); 96 | ``` 97 | 98 | java6 沒有內建推定型別,可透過推定型別函式來包裝,常見的 apache common-lang 或者 guava 函式庫也有此範例: 99 | 100 | ```java 101 | List names = newArrayList(); 102 | 103 | public static ArrayList newArrayList() { 104 | return new ArrayList(); 105 | } 106 | ``` 107 | 108 | ## IDE 推薦使用 Android Studio 而不是 Eclipse/ADT 109 | 110 | ... 111 | 112 | ## 推薦使用 gradle 建置系統而不是 ant 113 | 114 | 還有一些其他選項: 115 | 116 | * buck 117 | * maven 118 | * sbt 119 | * kobalt 120 | 121 | 有個基本原則,就是支援 maven 套件中心的建置系統 122 | 123 | ## 該傳遞 Context 就不要傳遞 Activity,避免記憶體浪費 124 | 125 | ```java 126 | ImageView imageView = new ImageView(activity.getApplicationContext()); 127 | ``` 128 | 129 | 不該: 130 | 131 | ```java 132 | ImageView imageView = new ImageView(activity); 133 | ``` 134 | 135 | 同樣的,函式庫的開發者,盡可能不要紀錄 activity: 136 | 137 | ```java 138 | public class ImageLoader { 139 | Context context; 140 | public ImageLoader(Activity activity) { this(activity.getApplicationContext()); } 141 | public ImageLoader(Context context) { this.context = context; } 142 | } 143 | ``` 144 | 145 | 參考樹可能類似: 146 | 147 | ``` 148 | applicationContext <- activity 149 | ``` 150 | 151 | 如果你只是需要 getResources() 之類的行為,並不需要 activity 整個實體,你可能只需要最基礎的 context 那麼就不需要握著 activity 不讓 gc 去釋出。 152 | 153 | 詳細的部份, 我們可以看 ContextWrapper/Impl 154 | 155 | ## 常見的取得 LayoutInflater 156 | 157 | ```java 158 | LayoutInflater.from(Context); 159 | ``` 160 | 161 | ## 常見的取得 context 162 | 163 | ```java 164 | view.getContext(); 165 | ``` 166 | 167 | ## UI 執行緒執行 168 | 169 | ```java 170 | activity.runOnUiThread(runnable); 171 | ``` 172 | 173 | ```java 174 | view.post(runnable); 175 | ``` 176 | 177 | ```java 178 | new Handler(Looper.getMainLooper()).post(runnable); 179 | ``` 180 | 181 | ## 使用 Objects 工具類別來簡化 null 檢查 182 | 183 | Before: 184 | 185 | ```java 186 | if (!mBuildConfigFields.equals(that.mBuildConfigFields)) { 187 | return false; 188 | } 189 | if (!mConsumerProguardFiles.equals(that.mConsumerProguardFiles)) { 190 | return false; 191 | } 192 | if (!mManifestPlaceholders.equals(that.mManifestPlaceholders)) { 193 | return false; 194 | } 195 | if (mMultiDexEnabled != null ? !mMultiDexEnabled.equals(that.mMultiDexEnabled) : 196 | that.mMultiDexEnabled != null) { 197 | return false; 198 | } 199 | if (mMultiDexKeepFile != null ? !mMultiDexKeepFile.equals(that.mMultiDexKeepFile) : 200 | that.mMultiDexKeepFile != null) { 201 | return false; 202 | } 203 | if (mMultiDexKeepProguard != null ? !mMultiDexKeepProguard.equals(that.mMultiDexKeepProguard) : 204 | that.mMultiDexKeepProguard != null) { 205 | return false; 206 | } 207 | if (!mProguardFiles.equals(that.mProguardFiles)) { 208 | return false; 209 | } 210 | if (!mResValues.equals(that.mResValues)) { 211 | return false; 212 | } 213 | return true; 214 | ``` 215 | 216 | After: 217 | 218 | ```java 219 | return Objects.equal(mBuildConfigFields, that.mBuildConfigFields) && 220 | Objects.equal(mConsumerProguardFiles, that.mConsumerProguardFiles) && 221 | Objects.equal(mManifestPlaceholders, that.mManifestPlaceholders) && 222 | Objects.equal(mMultiDexEnabled, that.mMultiDexEnabled) && 223 | Objects.equal(mMultiDexKeepFile, that.mMultiDexKeepFile) && 224 | Objects.equal(mMultiDexKeepProguard, that.mMultiDexKeepProguard) && 225 | Objects.equal(mProguardFiles, that.mProguardFiles) && 226 | Objects.equal(mResValues, that.mResValues); 227 | ``` 228 | 229 | ## 使用 Objects 工具類別來產生 hashcode 230 | 231 | Before: 232 | 233 | ```java 234 | int result = mBuildConfigFields.hashCode(); 235 | result = 31 * result + mResValues.hashCode(); 236 | result = 31 * result + mProguardFiles.hashCode(); 237 | result = 31 * result + mConsumerProguardFiles.hashCode(); 238 | result = 31 * result + mManifestPlaceholders.hashCode(); 239 | result = 31 * result + (mMultiDexEnabled != null ? mMultiDexEnabled.hashCode() : 0); 240 | result = 31 * result + (mMultiDexKeepFile != null ? mMultiDexKeepFile.hashCode() : 0); 241 | result = 31 * result + (mMultiDexKeepProguard != null ? mMultiDexKeepProguard.hashCode() : 0); 242 | return result; 243 | ``` 244 | 245 | After: 246 | 247 | ```java 248 | return java.util.Objects.hashCode( 249 | mBuildConfigFields, 250 | mResValues, 251 | mProguardFiles, 252 | mConsumerProguardFiles, 253 | mManifestPlaceholders, 254 | mMultiDexEnabled, 255 | mMultiDexKeepFile, 256 | mMultiDexKeepProguard); 257 | ``` 258 | 259 | ## 善用 @CallSuper 260 | 261 | ```java 262 | @CallSuper 263 | @Override 264 | public void onResume() { 265 | super.onResume(); 266 | } 267 | ``` 268 | 269 | ## 盡可能使用 `private final mMember` 270 | 271 | ## 成員變數名稱開頭,慣用小寫 "m" 開頭:`mMember` 272 | 273 | ## 不要直接 Handler 成員變數,避免記憶體浪費,可改用 WeakReference 包裝 274 | 275 | before: 276 | 277 | ```java 278 | private final Handler mHandler = new Handler(); 279 | ``` 280 | 281 | after: 282 | 283 | ```java 284 | private final WeakReference mHandler = new WeakReference<>(new Handler()); 285 | ``` 286 | 287 | ## static final 常數慣用大寫 288 | 289 | ```java 290 | static final float PI = 3.14f; 291 | ``` 292 | 293 | ## static 變數慣用小寫 "s" 開頭 294 | 295 | ```java 296 | private static Runtime sRuntime = new Runtime(); 297 | ``` 298 | 299 | ## 不要輕易使用 static 變數,避免記憶體浪費 300 | 301 | ```java 302 | static Drawable sBackground; 303 | ``` 304 | 305 | 非得要用了話,請用 WeakReference 裝起來: 306 | 307 | ```java 308 | static WeakReference sBackground; 309 | ``` 310 | 311 | 312 | ## 不要輕易使用 static 變數在 View 上,避免記憶體浪費 313 | 314 | ```java 315 | static TextView sView; 316 | ``` 317 | 318 | 因為 view 本身帶有 Context ,非得要用了話,請用 WeakReference 裝起來: 319 | 320 | ```java 321 | static WeakReference sView; 322 | ``` 323 | 324 | ## 工具類別應不給繼承且不給建構子 325 | 326 | ```java 327 | public final class Utils { 328 | private Utils() { 329 | throw new UnsupportedException(); 330 | } 331 | } 332 | ``` 333 | 334 | ## 回傳空 List 應該用 `return Collections.emptyList();` 而不是 `return new ArrayList<>();` 335 | 336 | ```java 337 | List getNames(List users) { 338 | if (users == null) return Collections.emptyList(); 339 | 340 | List names = new ArrayList<>(); 341 | for (User user : users) names.add(user.name()); 342 | return names; 343 | } 344 | ``` 345 | 346 | 回傳空 Map 應該用 `return Collections.emptyMap();` 而不是 `return new HashMap<>();` 347 | 348 | ```java 349 | Map getNames(List users) { 350 | if (users == null) return Collections.emptyMap(); 351 | 352 | Map map = new HashMap<>(); 353 | for (User user : users) map.put(user.id(), user); 354 | return map; 355 | } 356 | ``` 357 | 358 | ## 大量的字串串接,請用 StringBuilder 來避免不必要的低消 359 | 360 | ```java 361 | System.out.println("Hello" + ", " + "world" + "!"); 362 | ``` 363 | 364 | ```java 365 | new StringBuilder().append("Hello").append(", ").append("world").append("!"); 366 | ``` 367 | 368 | ## SystemService 取得方法,如 NotificationManager 請改用 `context.getSystemService(NotificationManager.class)` 369 | 370 | Before: 371 | 372 | ```java 373 | NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 374 | ``` 375 | 376 | After: 377 | 378 | ```java 379 | NotificationManager notificationManager = context.getSystemService(NotificationManager.class); 380 | ``` 381 | 382 | 好處是不用強轉型了。 383 | 384 | ```java 385 | // 另外推薦函式庫 com.github.yongjhih:android-system-services:1.0.0 386 | NotificationManager notificationManager = SystemServices.from(context).getNotificationManager(); 387 | ``` 388 | 389 | ## Listener/Callback 設計三兩事 390 | 391 | 就算我們只想要知道 `onPageSelected(position)` ,但是所有的方法還是都要假實現: 392 | 393 | ```java 394 | pager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { 395 | @Override 396 | public void onPageScrollStateChanged(int state) { 397 | } 398 | 399 | @Override 400 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 401 | } 402 | 403 | @Override 404 | public void onPageSelected(int position) { 405 | System.out.println(position); 406 | } 407 | }); 408 | 409 | public interface ViewPager.OnPageChangeListener() { 410 | void onPageScrollStateChanged(int state); 411 | void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); 412 | void onPageSelected(int position); 413 | } 414 | 415 | ``` 416 | 417 | 所以一般我們都習慣寫一個空類別來偷懶一下: 418 | 419 | ```java 420 | pager.setOnPageChangeListener(new SimpleOnPageChangeListener() { 421 | @Override 422 | public void onPageSelected(int position) { 423 | System.out.println(position); 424 | } 425 | }); 426 | 427 | public class SimpleOnPageChangeListener implements OnPageChangeListener { 428 | @Override 429 | public void onPageScrollStateChanged(int state) { 430 | } 431 | 432 | @Override 433 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 434 | } 435 | 436 | @Override 437 | public void onPageSelected(int position) { 438 | } 439 | } 440 | ``` 441 | 442 | 由於已經有 lambda 的幫助下,我們可以想到這個用法: 443 | 444 | ```java 445 | pager.setOnPageChangeListener(new SimplerOnPageChangeListener().onPageSelected(position -> { 446 | System.out.println(position); 447 | })); 448 | 449 | public class SimplerOnPageChangeListener implements OnPageChangeListener { 450 | @Override 451 | public void onPageScrollStateChanged(int state) { 452 | if (onPageScrollStateChanged == null) return; 453 | onPageScrollStateChanged.call(state); 454 | } 455 | 456 | @Override 457 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 458 | if (onPageScrolled == null) return; 459 | onPageScrolled.call(position, positionOffset, positionOffsetPixels); 460 | } 461 | 462 | @Override 463 | public void onPageSelected(int position) { 464 | if (onPageSelected == null) return; 465 | onPageSelected.call(position); 466 | } 467 | 468 | Action1 onPageScrollStateChanged; 469 | Action3 onPageScrolled; 470 | Action1 onPageSelected; 471 | 472 | public SimplerOnPageChangeListener onPageScrollStateChanged(Action1 onPageScrollStateChanged) { 473 | this.onPageScrollStateChanged = onPageScrollStateChanged; 474 | return this; 475 | } 476 | 477 | public SimplerOnPageChangeListener onPageScrolled(Action3 onPageScrolled) { 478 | this.onPageScrolled = onPageScrolled; 479 | return this; 480 | } 481 | 482 | public SimplerOnPageChangeListener onPageSelected(Action1 onPageSelected) { 483 | this.onPageSelected = onPageSelected; 484 | return this; 485 | } 486 | 487 | public SimplerOnPageChangeListener onPageChange(Action1 onPageSelected) { 488 | return onPageSelected(onPageSelected); 489 | } 490 | 491 | public SimplerOnPageChangeListener onPageChange(Action1 onPageSelected, Action1 onPageScrollStateChanged) { 492 | return onPageSelected(onPageSelected).onPageScrollStateChanged(onPageScrollStateChanged); 493 | } 494 | 495 | public SimplerOnPageChangeListener onPageChange(Action1 onPageSelected, Action1 onPageScrollStateChanged, Action3 onPageScrolled) { 496 | return onPageSelected(onPageSelected).onPageScrollStateChanged(onPageScrollStateChanged).onPageScrolled(onPageScrolled); 497 | } 498 | 499 | public interface Action1 { 500 | void call(T t); 501 | } 502 | 503 | public interface Action3 { 504 | void call(T t, T2 t2, T3 t3); 505 | } 506 | } 507 | ``` 508 | 509 | ## 泛型 Generic 應用於 Callback 510 | 511 | 如果你是設計一個 Github RESTful API 客戶端,可能會是這樣寫: 512 | 513 | ```java 514 | github.request("yongjhih/rxparse", new RequestListener() { 515 | @Override public void onComplete(String json) { 516 | List contributors = Contributors.parse(json); 517 | // ... 518 | } 519 | @Override public void onException(Exception e) { 520 | } 521 | }); 522 | 523 | github.request("yongjhih", new RequestListener() { 524 | @Override public void onComplete(String json) { 525 | List repositories = Repositories.parse(json); 526 | // ... 527 | } 528 | @Override public void onException(Exception e) { 529 | } 530 | }); 531 | 532 | interface RequestListener { 533 | void onComplete(String json); 534 | void onException(Exception e); 535 | } 536 | 537 | class GitHub { 538 | public void request(String endpoint, RequestListener listener) { 539 | // ... 540 | // listener.onComplete(json); or listener.onException(e); 541 | } 542 | } 543 | ``` 544 | 545 | 我們可以看到,每一個 callback 內都要寫一個 `parse()` 才得以使用,或許你會修改成: 546 | 547 | 548 | ```java 549 | github.contributors("yongjhih/rxparse", new ContributorsRequestListener() { 550 | @Override public void onComplete(List contributors) { 551 | // ... 552 | } 553 | @Override public void onException(Exception e) { 554 | } 555 | }); 556 | 557 | github.repositories("yongjhih", new RepositoriesRequestListener() { 558 | @Override public void onComplete(List repositories) { 559 | // ... 560 | } 561 | @Override public void onException(Exception e) { 562 | } 563 | }); 564 | 565 | interface ContributorsRequestListener { 566 | void onComplete(List contributors); 567 | void onException(Exception e); 568 | } 569 | 570 | interface RepositoriesRequestListener { 571 | void onComplete(List repositories); 572 | void onException(Exception e); 573 | } 574 | 575 | class GitHub { 576 | // ... 577 | public void contributors(String endpoint, ContributorsRequestListener listener) { 578 | request(endpoint, new RequestListener() { 579 | @Override public void onComplete(String json) { 580 | listener(Contributors.parse(json)); 581 | } 582 | @Override public void onException(Exception e) { 583 | listener(e); 584 | } 585 | }); 586 | } 587 | 588 | public void repositories(String endpoint, RepositoriesRequestListener listener) { 589 | request(endpoint, new RequestListener() { 590 | @Override public void onComplete(String json) { 591 | listener(Repositories.parse(json)); 592 | } 593 | @Override public void onException(Exception e) { 594 | listener(e); 595 | } 596 | }); 597 | } 598 | } 599 | ``` 600 | 601 | 這樣使用上確實有簡化了,但是這樣的 Listener 的數量就跟著回傳 model 種類成長,會造成維護上的繁瑣。 602 | 603 | 我們可以透過泛型來簡化,寫更通用一點介面: 604 | 605 | ```java 606 | github.contributors("yongjhih/rxparse", new SimpleRequestListener<>() { 607 | @Override public void onComplete(List contributors) { 608 | // ... 609 | } 610 | @Override public void onException(Exception e) { 611 | } 612 | }); 613 | 614 | github.repositories("yongjhih", new SimpleRequestListener<>() { 615 | @Override public void onComplete(List repositories) { 616 | // ... 617 | } 618 | @Override public void onException(Exception e) { 619 | } 620 | }); 621 | 622 | interface SimpleRequestListener { 623 | void onComplete(List list); 624 | void onException(Exception e); 625 | } 626 | 627 | class GitHub { 628 | // ... 629 | public void contributors(String endpoint, SimpleRequestListener listener) { 630 | request(endpoint, new RequestListener() { 631 | @Override public void onComplete(String json) { 632 | listener(Contributors.parse(json)); 633 | } 634 | @Override public void onException(Exception e) { 635 | listener(e); 636 | } 637 | }); 638 | } 639 | 640 | public void repositories(String endpoint, SimpleRequestListener listener) { 641 | request(endpoint, new RequestListener() { 642 | @Override public void onComplete(String json) { 643 | listener(Repositories.parse(json)); 644 | } 645 | @Override public void onException(Exception e) { 646 | listener(e); 647 | } 648 | }); 649 | } 650 | } 651 | 652 | ``` 653 | 654 | 這樣就可以維持一個 Listener 介面而已。 655 | 656 | 另外,對內維護上,如果有很多 API 要寫,複製貼上的程式碼片段仍然大了一點,所以再設計一個通用實做: 657 | 658 | ```java 659 | class GitHub { 660 | // ... 661 | public void request(String endpoint, SimpleRequestListener listener) { 662 | request(endpoint, new RequestListener() { 663 | @Override public void onComplete(String json) { 664 | listener(T.parse(json)); 665 | } 666 | @Override public void onException(Exception e) { 667 | listener(e); 668 | } 669 | }); 670 | } 671 | 672 | public void contributors(String endpoint, SimpleRequestListener listener) { 673 | request(endpoint, listener); 674 | } 675 | 676 | public void repositories(String endpoint, SimpleRequestListener listener) { 677 | request(endpoint, listener); 678 | } 679 | } 680 | 681 | interface Parsable { 682 | T parse(String json); 683 | } 684 | 685 | public class Contributor implements Parsable { 686 | public Contributor() {} 687 | public static List parse(String json) { 688 | // ... 689 | return contributors; 690 | } 691 | } 692 | ``` 693 | 694 | 留意,泛型目前還沒有重載(Overloading)的能力,所以不能有類似: 695 | 696 | ```java 697 | class JsonParser { 698 | String toJson(List repositories) { 699 | } 700 | 701 | String toJson(List Contributors) { 702 | } 703 | } 704 | ``` 705 | 706 | 需要表明泛型: 707 | 708 | ```java 709 | String contributorsJson = JsonParser.toJson(contributors); 710 | String repositoriesJson = JsonParser.toJson(repositories); 711 | 712 | class JsonParser { 713 | static String toJson(Collection list) { 714 | return Contributors.toJson(list); 715 | } 716 | static String toJson(Collection list) { 717 | return Repositories.toJson(list); 718 | } 719 | } 720 | ``` 721 | 722 | 723 | 或者改用介面取代重載: 724 | 725 | ```java 726 | class JsonParser { 727 | String toJson(Collection list) { 728 | return T.toJson(list); 729 | } 730 | } 731 | 732 | interface Jsonable { 733 | String toJson(Collection list); 734 | } 735 | 736 | public class Contributor implements Jsonable { 737 | public Contributor() {} 738 | public static String toJson(Collection list) { 739 | // ... 740 | return contributors; 741 | } 742 | } 743 | ``` 744 | 745 | ## 無參數具回傳值的介面: Callable 746 | 747 | ```java 748 | interface java.util.concurrent.Callable { 749 | V call(); 750 | } 751 | ``` 752 | 753 | ```java 754 | interface Runnable { 755 | void run(); 756 | } 757 | ``` 758 | 759 | -------------------------------------------------------------------------------- /eventbus-otto.md: -------------------------------------------------------------------------------- 1 | # EventBus: greenrobot/EventBus, Square/otto 2 | 3 | 通常用來處理跨 Activity, View, Thread 等溝通問題。 4 | 5 | 1. 通常遇到這種問題,一種是建立溝通管道,例如傳遞 callback 的方式。 6 | 2. 利用廣播方式,intent broadcast 7 | 3. 懶得傳遞 callback 就依靠全域變數註冊 callback. 8 | 9 | greenrobot/EventBus 就是走第三種方案。 10 | 11 | *我們都知道全域東西都有毒* 12 | 13 | ## greenrobot/EventBus vs. Otto 14 | 15 | 網路上有很多比較,可以參考。 16 | 17 | 這裡先給個簡單的參考: EventBus 比較快、 Otto 比較小。 18 | 19 | Otto 由知名開發商 Square 所維護,可靠度較高 20 | 21 | *如果已經引進 RxJava 可使用 RxJava 的 Subject 取代, RxRelay* 22 | 23 | ## 補充 - 回復性、耦合性 24 | 25 | 儲存問題, 可回復性. 如果透過 saveInstanceState 就可以交由系統負責儲存。一種是儲存在 disk 如: sharedPreferences. 再來是 DB 。 26 | 27 | 也就是 http://endlesswhileloop.com/blog/2015/06/11/stop-using-event-buses/ 提及的: 28 | 29 | > * Requires the data as a dependency. 30 | > * Handle the state where the data is not available. 31 | 32 | 不過 http://endlesswhileloop.com/blog/2015/06/11/stop-using-event-buses/ 標題過激,事實上,只要不要濫用,能夠正常建立 callback 溝通管道的就建立,不得已我們再來考慮 EventBus 。 33 | 34 | # See Also 35 | 36 | 有 memory leak 的問題: 37 | 38 | * [#57 Weak reference to the subscriber](https://github.com/greenrobot/EventBus/issues/57), 解法: [yongjhih/EventBus/commit/3d3c1ca6](https://github.com/yongjhih/EventBus/commit/3d3c1ca6676112bd9dd6bb78245b03b31e5c25fc) 39 | 40 | *Otto 名字的由來自辛普森家庭卡通中,校車的司機(bus driver) 就叫做 Otto* 41 | 42 | * https://github.com/greenrobot/EventBus 43 | * https://github.com/square/otto 44 | * http://endlesswhileloop.com/blog/2015/06/11/stop-using-event-buses/ 45 | -------------------------------------------------------------------------------- /facebook_sdk.md: -------------------------------------------------------------------------------- 1 | # Facebook SDK 2 | 3 | ## Graph -------------------------------------------------------------------------------- /faq.md: -------------------------------------------------------------------------------- 1 | # 經典問題 - 指引你「這該怎麼弄?」 2 | 3 | 這裡是從問題指引你到哪裡可以找到答案。或者曾經有人問過我的問題,這是我的備忘錄。 4 | 5 | ## gradle 如何只測試單項? 6 | 7 | ```bash 8 | ./gradlew testDebug --tests='*.' 9 | ``` 10 | 11 | ## 利用回傳空列表 `Collections.emptyList()` 來減少不必要的 null `List` 檢查 12 | 13 | * 不再 `return new ArrayList<>();` 改用 `Collections.emptyList()` 14 | * 不再 `return new HashSet<>();` 改用 `Collections.emptySet()` 15 | 16 | 不用檢查: 17 | 18 | ```java 19 | List infos = packageManager.queryIntentActivities(intent, flags); 20 | // infos == null? 像是官方文件就寫清楚肯定會回傳一個空列表,所以我們可以不用檢查 null 21 | for (info : infos) { 22 | System.out.println(info); 23 | } 24 | ``` 25 | 26 | ## 要怎麼寫非同步? 27 | 28 | * Thread 29 | * HandlerThread 30 | * AsyncTask 31 | 32 | ## 要怎麼避免 Callback Hell 33 | 34 | 使用 RxJava 或者 Bolts 等具 promise 架構來整平 35 | 36 | ## 按鈕不想讓別人連按,要怎麼寫 debounce? 37 | 38 | ## Restful client 要怎麼寫?用哪套? 39 | 40 | * Retrofit 41 | * Volley 42 | 43 | See Also: ... 44 | 45 | ## ImageLoader 要用哪套? 46 | 47 | 每套有其特性,一般短小精幹可用 picasso. 48 | 49 | * 大量客製調整 AUIL 50 | * Glide 效能考量 51 | 52 | See Also: ... 53 | 54 | ## Json 轉物件 要用哪套? 55 | 56 | * LoganSquare 目前是非 reflection 方式,所以應有較高的效能表現。 57 | * Jackson 58 | * Gson 59 | 60 | See Also: ... 61 | 62 | ## 我該從哪得知 Android App 開發的資訊? 63 | 64 | 前端 UI 、美工 Art 、平面字體挑選、換場動畫設計。 65 | 66 | App 工程開發。 67 | 68 | 函式庫資訊 69 | 70 | ## 沒有回傳值的 Callback 用 RxJava 要怎麼寫? 71 | 72 | > 想要問一下,我有一個 init() function 可能會做很久,所以我裡面開了一個 thread,然後 ui 傳入一個 callback 當我 init 完成之後我 callback 他。 73 | 74 | > 如果想要改成 RxJava 的架構,所以我 init 回傳了 Observable,這裡 Boolean 其實有點多餘,因為我只是要單純的通知 ui 說我做完了,感覺有點硬是要用 Observable 來做。 75 | 76 | 77 | 如果你用 `AsyncTask` 也會有類似的問題。 78 | 79 | ```java 80 | Observable initAsyncObs = Observable.defer(() -> Observable.just(initSync())) 81 | .subscribeOn(Schedulers.io()) 82 | .observeOn(AndroidSchedulers.mainThread()); 83 | 84 | Void initSync() { 85 | // ... 86 | return null; 87 | } 88 | ``` 89 | 90 | ## Background Thread API 用 RxJava `.observeOn(AndroidSchedulers.mainThread());` 好嗎? 91 | 92 | > api 回傳 Observable ,然後由 ui subscribe 來取得資料更新畫面 93 | > 而 Observable 先在 api 層設定好要在 background thread 執行,讓 ui 不用去管這段 94 | 不知道這樣的觀念是否正確,或是有更好的架構? 95 | 96 | 97 | Android Threading/Scheduling 很多不只是 background 的問題,還有 lifecycle 問題,LoaderManager 其實也有在處理 lifecycle 問題。 98 | 99 | 所以 Ui 還是要自己善用 `AppObservable.bindActivity(activity, obs)`, `AppObservable.bindFragment(fragment, obs)`, `ViewObservable.bindView(view, obs)` 這些就是負責處理在 Android 上的 LifeCycle 以及 Threading 問題。 (rxandroid) 100 | 101 | 102 | ## Ui Adapter 要用的,RxJava 回傳 `Observable>` 還是 `Observable` 103 | 104 | > 感覺回傳 List 就很鳥,但是如果 ui 就是要收集到全部的資料在一次刷新,回傳 list 對 ui 來說好像比較方便 105 | 106 | 為了 UI adapater 方便,是否傳遞 `Observable>`? 107 | 108 | 首先在函式庫角度來看,它應該只做到單純性、重用性、通用性、彈性、操作性的最大化。 109 | 110 | * 傳遞 `Observable>` 的劣勢,很明顯的是大部分的 Rx Operator 都無法直接使用,也就是我說的操作性很低。所以一般狀況下,不會傳遞無操作性的介面。 111 | 112 | 在這個前提下,自然會導出 -> 函式庫不提供 `Observable `。 113 | 114 | 接著是下一層的問題 ,那 Ui 該怎麼辦?很簡單,請服用: `Observable.toList()`, `Observable.buffer()`, etc. 115 | 116 | * See Also: https://kaif.io/z/compiling/debates/drg0Cf6oGj/dsCeOiO6Gz 117 | 118 | ## multidex 跟 proguard 擇一執行哪一個速度比較快? 119 | 120 | 先不管題目的合理性。 121 | 答案是 proguard 比較快。 122 | 123 | 1. 理論上,經過 proguard 之後,code size 的縮減,所以啟動程式碼載入時間會縮短。 124 | 2. multidex 還會變慢,因為 65k 超出去的部份是啟動之後,動態載入的,你知道的,就是那個 reflection ,想當然爾會更慢。 125 | 126 | 而提問者其實後來要問的是「編譯」而不是「執行」,如果是編譯,multidex 會快些。而我們本來在開發上就是 release build 才會跑 proguard minify ,所以倒不用擔心。 127 | 128 | ## abstract class 與 interface 有什麼不同? 129 | 130 | 坊間很多地方可以看到這個問題, 131 | 不推薦給初學者從這個問題著手去了解 abstract class 與 interface 存在的意義,因為這個問題誤導初學者。 132 | 但是作為一個面試題目,卻不失誘導應試者發揮看法。 133 | 134 | * abstract class 是一個功能不完整的半成品 135 | * interface 是待完成功能的集合體 136 | 137 | 讓我們舉個例子對照來促進思考: 138 | 139 | Before: 140 | 141 | ```java 142 | abstract class BaseButton { 143 | public abstract onClick(View view); 144 | } 145 | ``` 146 | 147 | After: 148 | 149 | ```java 150 | interface OnClickListener { 151 | public onClick(View view); 152 | } 153 | 154 | abstract class BaseButton2 implements OnClickListener { 155 | } 156 | ``` 157 | 158 | 也就是任何 abstract class 你都可以抽出一個 interface ,這樣就排除了一個誤導:abstract class 與 interface 不應該是對等的比較,也就是這是個偽命題。應該要問的是「哪時候使用 abstract class 還是 interface + class」。 159 | 160 | * *題外話: 這也就是筆者常拿來質疑 abstract class 修飾子的存在意義* 161 | * *解決多重繼承問題:如果你不知道什麼是「多重繼承」,就不需要去了解 interface 是如何解決的,因為對你來說是不存在的問題,很有可能因此混淆* 162 | * *「abstract class 不能 new 」並不是一個事實,如果是一個事實,那麼 interface 也不能 new ,所以並不是不同之處。而任何物件在建構時,都務必實現所有功能才能建構,abstract class 你可以:`new AsyncTask() { @Override xxx() ... }`,interface 也可以 `new OnClickListener() { @Override xxx() ... }`* 163 | * *「interface 不能繼承,只有 abstract class 可以繼承」這也不是事實,這應該是想要說 abstract class 通常被預期來拿來繼承使用,否則只能建構時馬上實做來使用* 164 | 165 | abstract class 被繼承或者建構時,必須實做所有未完成的方法,否則依然還是一個 abstract class。class + interface 在建構時,不需要立即實現,通常是以事後賦予的方式呈現。 166 | 167 | abstract class: 168 | 169 | ```java 170 | abstract class BaseButton { 171 | public abstract onClick(View view); 172 | } 173 | 174 | BaseButton button = new BaseButton() { 175 | @Override public onClick(View view) { 176 | System.out.println("click: " + view); 177 | } 178 | }; 179 | ``` 180 | 181 | class + interface: 182 | 183 | ```java 184 | class Button { 185 | OnClickListener mOnClickListener; 186 | 187 | public void setOnClickListener(OnClickListener onClickListener) { 188 | mOnClickListener = onClickListener; 189 | } 190 | 191 | private void click(View view) { 192 | mOnClickListener.onClick(view); 193 | } 194 | } 195 | 196 | Button button = new Button(); 197 | 198 | button.setOnClickListener(new OnClickListener() { 199 | @Override public onClick(View view) { 200 | System.out.println("click: " + view); 201 | } 202 | }); 203 | ``` 204 | 205 | abstract class 特性: 206 | 207 | * 依賴性:使用時必須實做,類似建構子的參數,建構依賴 208 | * 流程引用性:abstract method 是否都要被 parent 引用? 209 | 210 | 習性: 211 | 212 | * interface + class: 跨部門的功能,你就應該抽出 interface 來請大家實現。對老手來說,是一個拉平繼承樹或者倒裝繼承樹的手法 213 | * 當你寫了一個類別希望給大家繼承來用,但是建構時,怕你忘記做一些前置作業,就會使用 abstract class 214 | 215 | 以客觀來說,abstract class 用於分段實做。 216 | 217 | 以慣例來說,parent 制定大部分流程,必要流程請子嗣實現。 218 | 219 | ```java 220 | class DownloadTask extends AsyncTask { 221 | @Override protected File doInBackground(String... urls) { 222 | try { 223 | HttpRequest request = HttpRequest.get(urls[0]); 224 | File file = null; 225 | if (request.ok()) { 226 | file = File.createTempFile("download", ".tmp"); 227 | request.receive(file); 228 | publishProgress(file.length()); 229 | } 230 | return file; 231 | } catch (HttpRequestException exception) { 232 | return null; 233 | } 234 | } 235 | 236 | @Override protected void onProgressUpdate(Long... progress) { 237 | Log.d("DownloadTask", "Downloaded bytes: " + progress[0]); 238 | } 239 | 240 | @Override protected void onPostExecute(File file) { 241 | if (file != null) { 242 | Log.d("DownloadTask", "Downloaded file to: " + file.getAbsolutePath()); 243 | } else { 244 | Log.d("DownloadTask", "Download failed"); 245 | } 246 | } 247 | } 248 | 249 | new DownloadTask().execute("http://google.com"); 250 | ``` 251 | 252 | 以另一個慣例來說,parent 制定流程,操作的實體由子嗣實現。 253 | 254 | ```java 255 | class SimpleActivity extends BaseActivity { 256 | @Override public int getContentView() { 257 | return R.layout.main; 258 | } 259 | } 260 | 261 | abstract class BaseActivity extends Activity { 262 | public abstract int getContentView(); 263 | 264 | @Override public void onCreate(Bundle savedInstanceState) { 265 | super.onCreate(savedInstanceState); 266 | setContentView(getContentView()); 267 | } 268 | } 269 | ``` 270 | 271 | 272 | ## 什麼是泛形(generic) 以及 `` 與 `` 的使用時機 273 | 274 | 我們可以用來簡化轉型步驟: 275 | 276 | Before: 277 | 278 | ```java 279 | interface OnClickListener { 280 | void onClick(View view); 281 | } 282 | 283 | MyActivity extends Activity { 284 | // ... 285 | imageView.setOnClickListener(new OnClickListener() { 286 | @Override public void onClick(View view) { 287 | ImageView iv = (ImageView) view; 288 | iv.setImageResource(R.drawable.clicked); 289 | } 290 | }); 291 | } 292 | ``` 293 | 294 | After: 295 | 296 | ```java 297 | interface OnClickListener { 298 | void onClick(T view); 299 | } 300 | 301 | 302 | class ImageView extends View { 303 | OnClickListener mOnClickListener; 304 | 305 | public void setOnClickListener(OnClickListener onClickListener) { 306 | mOnClickListener = onClickListener; 307 | } 308 | } 309 | 310 | class MyActivity extends Activity { 311 | // ... 312 | imageView.setOnClickListener(new OnClickListener() { 313 | @Override public void onClick(ImageView iv) { 314 | iv.setImageResource(R.drawable.clicked); 315 | } 316 | }); 317 | } 318 | ``` 319 | 320 | `` 的使用時機: 321 | 322 | ```java 323 | interface Collection ... { 324 | E get(int index); 325 | void addAll(Colletion items); 326 | } 327 | ``` 328 | 329 | `` 的使用時機: 330 | 331 | ```java 332 | ``` 333 | 334 | 335 | 340 | 341 | ## 沒有 Http Library 直接操作 Socket 會怎麼做? 342 | 343 | 或許要問的是,對 Socket 本身的看法吧,我把 Socket 當作一種檔案流,實際上在 POSIX 系統就真的是檔案(其實什麼都檔案),你先建立一個 listening socket file by ip/port 去聽 (bind and listen),當然人要丟資料的時候,會開啟一個獨立的 connection socket file (accept),然後就可以開始讀檔了 `while (inputStream)`。 344 | 345 | 346 | 但是萬一後面還有人要進來,就沒法聽到了怎麼辦? 347 | 348 | ```java 349 | var binder = bind(address); 350 | while (binder.listen()) { // blocking until requested 351 | new Thread(() -> { // 馬上開一個 thread 去處理讀資料流 352 | var socket = binder.accept(); 353 | read(socket); 354 | // business logic 355 | write(socket); 356 | })); 357 | // 得以繼續等待下一個 request 358 | } 359 | ``` 360 | 361 | 下一個問題,如果很多 reuqest 進來,會不會來不及 `new Thread` 然後下一位的 request 沒聽到怎麼辦? 362 | 363 | 或許可以一開始就開 multithread 去 listent and accept 額定一個 thread 量,為了要重用 thread 還要開個 thread pool 。 364 | 365 | 而客戶端的部份,一樣主要 Socket to In/OutputStream ,只是因為網路存取通常是長時間存取,所以通常會開一個 Thread 出去避免 blocking main-thread ,那麼這種 io/net thread 為了要重用,也有 io/net thread pool 。 366 | 367 | ## Multi-Process 與 Multi-Thread 選擇 368 | 369 | 如果是只為了 non-blocking: 370 | 371 | 像是單純的網路服務,process 初始化的低消較大,如果用 fork 更開了副本,也有損耗問題,不過好處是 process management 獨立性/隔離性,萬一發生什麼問題不會影響到其他人。 372 | 373 | 而如果有需要交換資料且頻繁,就用 Multi-Thread 吧,否則 Multi-Process 你交換資料就只能 IPC 基本上都是透過 socket ,不然你就跟 Android 一樣做一個 amem 做 binder 幫你 marshall/unmarshall/de/serialize 來減輕低消。 374 | 375 | 像是 Android Service 宣告,會幫裝在 Process ,透過 AIDL ,generating Stub ,讓 binder 做資料交換,只要宣告 interface 即可。 376 | 377 | 系統應該要有一個機制去降低 fork process 低消,例如 COW 技術,在開啟新的 process 的時候,並沒有即時產生副本,頂多產生副本 refs ,當有人寫入的時候才進行實體副本。 378 | 379 | ## Android Observable 與 Button 380 | 381 | Observable 表示可被觀測,所以你可以塞一個觀察者 Observer 進去,聆聽一些變動。而 Button 可以 binding 一些變動行為。 382 | 383 | 一般 Android 實務都是你的某個 ContentProvider 實現了 Observable ,所以你當然可以註冊 Observer 進去,一般也繼承 ContentObsever,為什麼要繼承 ContentObserver ?因為可以給 key 進去,讓 ContentProvider calling back by key 阿。 384 | 385 | 以 RxJava 撰寫風格來說: 386 | 387 | ``` 388 | getProviderSubject(key).asObservable().subscribe(changed -> changed ? button.on() : button.off()); 389 | ``` 390 | 391 | 稍微接近一點 Android 的寫法,但是我習慣接龍(Fluent): 392 | 393 | ```java 394 | ContentResolvers.select(key).subscribe(changed -> changed ? button.on() : button.off()); 395 | ``` 396 | 397 | ref. https://github.com/yongjhih/content-observables 398 | 399 | ## Activity Lifecycle 的排定是為什麼? 400 | 401 | 為了應用以及不傷身體,為了節省資源回收的時候狀態要告知,才能夠不傷身體。應用則是一些狀態告知可以應用 onPause/onResume ,較多的排定也是為了靈活手機的應用以及硬體限制需要回收的告知對策。 402 | 403 | ## 隨便 kill 無所謂? 404 | 405 | kill 照理說要作到不傷身體,你就該好好告知你的客戶(application),也有很多 signal 可以 trap ,所以安排了很多告知方法:lifecycle。 406 | 除非你是用 -9 signal ,但是萬一因此傷身,這個應該是發起者後果自負。 407 | 408 | ## LRUCache 409 | 410 | 像是行車記錄器會把最後的刪除。如果要實現,沒多想,直接用 mod index 。 411 | 412 | 不過這樣讀取就太慢了,如果仍然使用 Map 來裝,要有順序 order index , on-put dereference 。 413 | 414 | * p.s. *當天路上忽然想到,好像沒人糾正說講錯,不叫做 push/pop ,有點壞心啊,腦袋關聯想左推右彈,嘴巴稱之 push/pop,但是這是錯誤的用詞,也許因為我之前寫過一個 SimpleLruCache 給 [simple-parse/f082efe2](https://github.com/yongjhih/simple-parse/commit/f082efe2ce46f40b8b7cd80a50d3d65652fc4ad7) 用,裡面一堆 map.put() ?不過回想起來這好蠢喔。如果要寫,核心想法就是 mod index ,反正沒 ref 就 gc,不過讀取應該就慢爆了吧* 415 | 416 | ## Media Server 417 | 418 | froyo 以前叫做 opencore ,後來叫做 stagefright 。年代久遠,基本上還是要回去看我 [patches](https://drive.google.com/open?id=0B0Q2kL5yaBdRNmtnREFZWUVvS1k) 才會會想起在幹麻了(froyo 的 patches 似乎已經丟失)。 419 | 420 | 反倒是 camera layer 有點印象, camera service , jni , libcamera, hal mm-camera, v4l2 421 | 422 | ## Java 語言使用上的問題 423 | 424 | 問題挺多的,就看我寫的一些雞肋函式庫就知道了,不然看看 kotlin 那篇介紹也可以,像是 lambda , pojo getter/setter , nullable/optional, return-type overloading, generic infer 425 | 426 | ## 會用 jenkins 嗎? 427 | 428 | * 2011 年開始使用 Jenkins 集合 Gerrit 自動 verify : https://docs.google.com/document/d/1_H2AW-jrpugcq29h8-Uq4ViNnFPSfY9b1zSy3Cttg6w/ 429 | * 2011-2013 on-push/on-upload 自動建置 aosp 與其 cts 430 | * 2013-2016 on-push 自動建置 apk 431 | 432 | ## 對於 devops 的看法 433 | 434 | * 從開發流程到檢驗到佈署的自動化/最佳化 435 | * 在這之前你應該為了彈性抽換,會有虛擬化/隔離化 e.g. lxc 436 | * 到資源管理 scheduling 437 | * 到成本管理 438 | 439 | ## 測試 440 | 441 | 優先把重用性高的拆出去成獨立的 lib ,既然已經拆出去,解藕,那麼 Unit Test 就會很好寫了。而商業邏輯在新創公司經常性變動,也就是程式區段生命週期短的部份就不用刻意寫自動化測試,直到穩定成為週期長的程式區段,再進行自動化測試的撰寫。 442 | 443 | ## 優先做出東西還是優先找最佳解 444 | 445 | * 優先做出雛型 - 這要看實際耗時問題,雖然希望優先做出雛型,但是其實往往工程師本位,還是會希望找出正解,否則怎麼會用 RxJava/retrofit/ORM?一開始的 restful client 需求肯定很低的,Model 也少,然後 rx 來做 pipeline 也不多,會什麼要導入 rxjava/retrofit/ORM ? 直接 `db.exec(sql)` 不就好了,直接 `httpclient.get(url)` 取幾個 json 欄位,很快就可以寫好。 446 | * 不過找最佳解的途中,也許會發現那個東西根本有問題,也就是在釐清問題,收斂問題。 447 | 448 | ## WindowManager 449 | 450 | 現在我還沒什麼印象,只記得 Phone xx Manager ,跟改過 KeyGuard 451 | 452 | ## ViewHirarchy TouchEvent 453 | 454 | 從樹根傳遞,像是 nested scrollview 常需要 Parent Reqesut disallow Inercept 455 | 456 | ## 攤平 callback hell 的一些想法 457 | 458 | 在談論 Rx 的時候,常常跟朋友提到 callback hell 問題與解法 459 | 460 | Before: 461 | 462 | ```java 463 | loginFacebook(fbToken -> { 464 | loginParse(lastFbToken, parseToken -> { 465 | loginOctory(lastParseToken, octoryToken -> { 466 | }); 467 | }); 468 | }); 469 | ``` 470 | 471 | 我對於 callback hell 想要攤平,其實比較簡單的概念就是 register 模式: 472 | 473 | After: 474 | 475 | ```java 476 | Tasks tasks = Tasks.create(); 477 | tasks.add(task -> loginFacebook(fbToken -> { task.next(fbToken); })); 478 | tasks.add(task -> loginParse(task.get(), parseToken -> { task.next(parseToken); })); 479 | tasks.add(task -> loginOctory(task.get(), octoryToken -> { task.next(octoryToken); })); 480 | tasks.execute(); 481 | ``` 482 | 483 | 只有型別的部份比較麻煩,所以通常透過 generic infer 來做: 484 | 485 | After: 486 | 487 | ```java 488 | Task.from(() -> loginFacebook()).then(profile -> loginParse(profile)).then(profile -> loginOctory(profile)); 489 | ``` 490 | 491 | 如果這樣寫完,其實就完成了 promise 規範了。 492 | -------------------------------------------------------------------------------- /firebase.md: -------------------------------------------------------------------------------- 1 | # Firebase 2 | 3 | ## 前言 4 | 5 | Firebase 成立於 2011 年,主要提供 BaaS 的服務,當時的競爭對手有非常戲劇化落幕的 Parse 等。 6 | Firebase 於 2014 年加入了 Google 旗下,繼續他的 BaaS 服務,不過在 2016 Google IO 大會上,Google 給予了新的生命 - 幫助開發者可以 **更容易的開發,成長以及賺錢**。 7 | 8 | 這有什麼了不起的?為什麼我需要他? 9 | 10 | 在看完了 Firebase 的官網介紹之後,他非常強調一件事情,就是**讓數字來幫助我們做決策** 11 | 12 | 下圖為 Firebase 的新藍圖,由許多服務結合而成,可以看到 Analytics 是最重要的核心,而 BaaS 只是其中的一部分 13 | 14 | ![Firebase](https://lh3.googleusercontent.com/you5Qm6B9GhkBvQ-A25p2p3iDsRCzwbqupJ-H4wJWAnkl2O0jOgar4zhY31e0RUAw40P47jkfDg24T3KHDRFSFFRGUXn6a8=s888) 15 | 16 | 好,大家或許會好奇,這些服務現在都有現成的,而且大部分 Google Service Api 也都有提供,他為何要造一樣的輪子?我們真的有必要去使用它嗎? 17 | 18 | 先告訴大家,有,而且 Google 給了一個無法拒絕的 Offer 19 | 20 | ![alt](./firebase_update.jpg) 21 | 22 | 上圖可以看到 App Indexing 已經交給 Firebase 來處理了,假如想要使用最新的 Google Play Service API,Firebase 是避不了的! 23 | 24 | 當然除了上述理由外,Firebase 最重要的是提供許多實用的功能來幫助 App 成長茁壯,而且所有模組都圍繞在最核心的功能:Analytics, 25 | 用數字來幫助我們決策,不再只是一昧的去猜想修改程式,讓數字來說話吧! 26 | 27 | PS1. 本篇文章會專注在介紹 Firebase 為我們開發上帶來什麼好處,假如想了解 Firebase 如何導入,建議直接上官網了解! 28 | 29 | PS2. 因為不是每樣功能都有使用,所以會先針對目前使用的服務做分享 30 | 31 | ## Analytics 32 | // TODO 33 | 34 | ## Cloud Messaging & Notifications 35 | // TODO 36 | 37 | ## Crash Reporting 38 | 和許多知名 Crash 工具如 Crashlytics 串接上差不多,不過比較令人驚艷的地方是 39 | 他不只顯示基本的 Crash 資訊, 40 | 還很貼心的將使用者如何點擊的 Event 呈現出來(不過前提是你有埋到這些 Event), 41 | 更方便我們去思考相對應的解法(不過有依然很多無解啊 XD) 42 | 43 | ![alt](https://1.bp.blogspot.com/-OR6aq1Rtwzo/WEmfXoIrSJI/AAAAAAAAAps/ig171K0d960Hf8YZl0C5s2xO4DZICxQeACLcB/s1600/image03.png) 44 | 45 | 有興趣的也可以參考這篇[官方文章](https://firebase.googleblog.com/2016/12/firebase-crash-reporting-full-release.html) 46 | 47 | ## Remote Config 48 | // TODO 49 | 50 | ## App Indexing 51 | // TODO 52 | 53 | ## Dynamic Links 54 | // TODO 55 | 56 | ## Invites 57 | // TODO 58 | 59 | -------------------------------------------------------------------------------- /firebase_update.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongjhih/android-gitbook/53c322818684527c40f46c82fcdcaefbd2d76243/firebase_update.jpg -------------------------------------------------------------------------------- /flow_mortar.md: -------------------------------------------------------------------------------- 1 | # flow + mortar 2 | 3 | 從 2014/01 Square 部落格初次登板 [mortart and flow](https://corner.squareup.com/2014/01/mortar-and-flow.html) 的開頭可以得知, 4 | 一開始要解決的問題是 Fragment 換頁問題。 5 | 6 | 從 Android 4.0 之後大量改成 Fragment 導致一個 Activity 要掌管 Fragment transaction 換頁。 7 | 8 | 在一般用途上,筆者偷懶直接用 ViewPager + FragmentStatePagerAdapter + ViewPager.Transformer + 特製的 PagerIndicator ,基本上堪用。 9 | 10 | ## FragmentMaster 11 | 12 | 另一種 ViewPager + FragmentStatePagerAdapter + ViewPager.Transformer 的整合方案。 13 | 14 | ## See Also 15 | 16 | * https://github.com/square/flow/ 17 | * https://corner.squareup.com/2014/01/mortar-and-flow.html 18 | * https://www.bignerdranch.com/blog/an-investigation-into-flow-and-mortar/ 19 | * https://github.com/WeMakeBetterApps/MortarLib 20 | -------------------------------------------------------------------------------- /gradle.md: -------------------------------------------------------------------------------- 1 | # gradle 建置系統 2 | 3 | 常見的建置系統: 4 | 5 | * ant 6 | * buck 7 | * maven 8 | * gradle 9 | * sbt 10 | * kobalt 11 | 12 | 大多數的樣貌 build.gradle: 13 | 14 | ```gradle 15 | buildscript { // 建置設定區 - 引入建置相關插件庫 16 | repositories { 17 | jcenter() // 建置套件庫 18 | } 19 | dependencies { 20 | classpath 'com.android.tools.build:gradle:1.2.3' // 插件庫 21 | } 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | 26 | repositories { 27 | jcenter() // 函式套件庫 28 | } 29 | 30 | dependencies { 31 | //compile '{group}:{artifact}:{version}' 32 | //compile project('{module}') 33 | } 34 | 35 | android {} // com.android.application 插件設定區 36 | ``` 37 | 38 | 多模組的目錄結構: 39 | 40 | ``` 41 | -a-project 42 | |--build.gradle // 一般空檔, 除非需要子模組共用的設定,可以在這裡設定 43 | |--a-module/build.gradle 44 | |--b-module/build.gradle 45 | ``` 46 | 47 | 設定預設編譯哪些 module: 48 | 49 | settings.gradle: 50 | 51 | ```gradle 52 | include ':a-module' 53 | include ':b-module' 54 | // or include ':a-module', 'b-module' 55 | ``` 56 | 57 | 設定外部路徑: 58 | 59 | ```gradle 60 | // ... 61 | include ':b-c-module' 62 | project(':b-c-module').projectDir = new File(settingsDir, '../b-project/c-module') 63 | ``` 64 | 65 | build.gradle: 66 | 67 | ```gradle 68 | // ... 69 | dependencies { 70 | // ... 71 | compile project(':b-c-module') 72 | } 73 | // ... 74 | ``` 75 | 76 | 77 | ## 有哪些編譯項目可使用 78 | 79 | ```sh 80 | ./gradlew tasks 81 | ``` 82 | 83 | ## 設定快取有效時間 84 | 85 | 預設 24 小時,每天一開始的編譯都會比較久。為了避免這種情形,可以拉長時間,如有必要再透過強制刷新來解決。 86 | 87 | 寫到專案設定: 88 | 89 | ```gradle 90 | configurations.all { 91 | resolutionStrategy { 92 | cacheDynamicVersionsFor 30, 'days' 93 | cacheChangingModulesFor 30, 'days' 94 | } 95 | } 96 | ``` 97 | 98 | ## 強制刷新套件 99 | 100 | 如果有些套件像是 SNAPSHOT.jar 剛更新,可透過 `--refresh-dependencies` 來刷到新的版本: 101 | 102 | ```gradle 103 | ./gradlew --refresh-dependencies assembleDebug 104 | ``` 105 | 106 | ## 顯示詳細的測試項目通過與失敗 107 | 108 | ```gradle 109 | tasks.withType(Test) { 110 | testLogging { 111 | exceptionFormat "full" 112 | events "passed", "skipped", "failed", "standardOut", "standardError" 113 | showStandardStreams = true 114 | } 115 | } 116 | ``` 117 | 118 | ## 一般測試 119 | 120 | ```bash 121 | ./gradlew testDebug 122 | ``` 123 | 124 | ## 測試單項 125 | 126 | ```bash 127 | ./gradlew testDebug --tests='*.' 128 | ``` 129 | 130 | or 131 | 132 | ```bash 133 | ./gradlew -Dtest.single=ClassUnderTest test 134 | ``` 135 | 136 | ## 顯示更多 lint 警告 137 | 138 | ```gradle 139 | tasks.withType(JavaCompile) { 140 | options.compilerArgs << "-Xlint:deprecation" << "-Xlint:unchecked" 141 | } 142 | ``` 143 | 144 | ## 安裝 gradle wrapper by gradle 145 | 146 | ```gradle 147 | gradle wrapper --gradle-version 2.13 148 | ``` 149 | 150 | ## 安裝 gradle wrapper by docker gradle 151 | 152 | ```bash 153 | docker run -it -v $(pwd):/src yongjhih/gradle gradle wrapper --gradle-version 2.13 154 | ``` 155 | 156 | ## 升級 gradle wrapper 157 | 158 | 修改 gradle/wrapper/gradle-wrapper.properties: 159 | 160 | ```gradle 161 | ... 162 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip 163 | ``` 164 | 165 | ## ref. 166 | 167 | * https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.ResolutionStrategy.html 168 | -------------------------------------------------------------------------------- /image_loader_android-universal-image-loader,_picasso,_glide,_fresco.md: -------------------------------------------------------------------------------- 1 | # Image Loader - Android-Universal-Image-Loader, picasso, glide, fresco 2 | 3 | 例如圖片網址: `http://upload.wikimedia.org/wikipedia/commons/thumb/7/72/Flag_of_the_Republic_of_China.svg/125px-Flag_of_the_Republic_of_China.svg.png` 4 | 5 | Before: 6 | 7 | ```java 8 | new AsyncTask { 9 | @Override 10 | protected Bitmap doInBackground(String... urls) { 11 | String url = urls[0]; 12 | Bitmap bitmap = null; 13 | try { 14 | InputStream in = new java.net.URL(url).openStream(); 15 | bitmap = BitmapFactory.decodeStream(in); 16 | } catch (Exception e) { 17 | e.printStackTrace(); 18 | } 19 | return bitmap; 20 | } 21 | 22 | @Override 23 | protected void onPostExecute(Bitmap result) { 24 | if (result != null) mImageView.setImageBitmap(result); 25 | } 26 | }.execute("http://upload.wikimedia.org/wikipedia/commons/thumb/7/72/Flag_of_the_Republic_of_China.svg/125px-Flag_of_the_Republic_of_China.svg.png"); 27 | ``` 28 | 29 | After: 30 | 31 | AUIL(Android-Universal-Image-Loader): 32 | 33 | ```java 34 | ImageLoader.getInstance().displayImage("http://upload.wikimedia.org/wikipedia/commons/thumb/7/72/Flag_of_the_Republic_of_China.svg/125px-Flag_of_the_Republic_of_China.svg.png", mImageView); 35 | ``` 36 | 37 | Picasso: 38 | 39 | ```java 40 | Picasso.with(context).load("http://upload.wikimedia.org/wikipedia/commons/thumb/7/72/Flag_of_the_Republic_of_China.svg/125px-Flag_of_the_Republic_of_China.svg.png").into(mImageView); 41 | ``` 42 | 43 | Glide: 44 | 45 | ```java 46 | Glide.with(context).load("http://upload.wikimedia.org/wikipedia/commons/thumb/7/72/Flag_of_the_Republic_of_China.svg/125px-Flag_of_the_Republic_of_China.svg.png").into(mImageView); 47 | ``` 48 | 49 | Fresco: 50 | 51 | ```java 52 | mImageView.setImageURI(Uri.parse("http://upload.wikimedia.org/wikipedia/commons/thumb/7/72/Flag_of_the_Republic_of_China.svg/125px-Flag_of_the_Republic_of_China.svg.png")); 53 | ``` 54 | 除了寫法的簡便之外,為什麼要 Image Loader ,什麼是 Image Loader ? 55 | 56 | ImageLoader 會很聰明的知道 ImageView 在可視範圍內才去做下載與解壓縮 bitmap 一旦離開就停止一切作業。而且為了二次快速顯示,依據 ImageView 的可視長寬作參考最適 cache 。 57 | 58 | 1. 顯示快取 - 首先,為了再次顯示一樣的圖片時,可以快速顯示,所以我們會把 bitmap 暫存在記憶體 memory cache 。那在有限的 memory cache ,我們要如何管理 ? 常見的是 LRU cache 策略,memory cache 有限,最近看過的優先留下來。以及依據畫布大小的快取。 59 | 2. 儲存快取 - 再來,網路來的圖片、網址圖片,下載儲存快取管理 - Disk Cache。 60 | 3. 連線快取 - okhttp SPDY 61 | 62 | 在 fresco 出現之前,會比較推崇 AUIL(Android Universal Image Loader),接著 glide 、picasso 。 63 | 64 | 追求小 code size 可以選 picasso ,輕簡。 65 | 66 | AUIL 是因為在記憶體、儲存空間的快取策略還有其它有的沒有的都可以訂製。彈性比較大一點(所以 code size 大一點)。 67 | 68 | 只有 fresco 需要更換 layout class 原因是因為它為了效能,操作較低階的畫布。(Layout Class 都換了,所以順便支援 Gif 、影片? 誤) 69 | 70 | 對於 Image Loader 常見的需求: 71 | 72 | * 更換 OkHttp 下載器 73 | * 潤角、圓圖、顯示動畫 74 | * 支援 Exif 轉正,支援影片快照轉正 75 | 76 | Fresco - facebook 77 | 78 | * ImagePipeLine - 更換 OkHttp 下載器 (有 bug ,已解 [PR#21](https://github.com/facebook/fresco/pull/21)) 79 | 80 | picasso - square 81 | 82 | * Factory -> 更換 OkHttp 下載器 83 | * Transformer -> 潤角、圓圖、顯示動畫 84 | 85 | Android-Universal-Image-Loader 86 | 87 | * ImageDownloader -> 更換 OkHttp 下載器 88 | * ImageDisplayer -> 潤角、圓圖、顯示動畫 89 | * ImageDecoder -> 支援 Exif 轉正,支援影片轉正 90 | 91 | ## glide 92 | 93 | glide 許多 Google 官方 samples 都可看到它的蹤影,所以基本上 Google 有挺。 94 | 95 | ## 附錄 - 虛擬網址 - Facebook 真實圖片轉址 96 | 97 | 如果你的 ImageLoader 針對網路圖片,不支援轉址,又或者網址藏在 json 裡。 98 | ImageLoader 大多支援 ContentProvider 網址,`content://` 所以我們可以利用它來做虛擬網址轉址。 99 | 100 | *p.s. `https://graph.facebook.com/{id}/picture` 雖然本身有轉址能力,不過這裡為了教學所需,還是寫了一個 FacebookPictureProvider 作本地轉址範例。* 101 | 102 | 例如: 103 | 104 | GET `https://graph.facebook.com/601234567/picture?redirect=0`: 105 | 106 | ```json 107 | { 108 | "data": { 109 | "is_silhouette": false, 110 | "url": "https://fbcdn-profile-a.akamaihd.net/hprofile-ak-xpf1/v/t1.0-1/p50x50/1234567" 111 | } 112 | } 113 | ``` 114 | 115 | data.url 才是真實的圖片網址。 116 | 117 | 所以我們註冊一個內部網域: 118 | 119 | `content://com.facebook.content.PictureProvider/601234567` 120 | 121 | 提供轉址到真實圖片網址: 122 | 123 | `https://fbcdn-profile-a.akamaihd.net/hprofile-ak-xpf1/v/t1.0-1/p50x50/1234567` 124 | 125 | ```java 126 | public class FacebookPictureProvider extends NetworkPipeContentProvider { // NetworkPipeContentProvider 是筆者包裝過的,可參下方 github 127 | public static final String AUTHORITY = "com.facebook.provider.PictureProvider"; 128 | public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/"); 129 | private Facebook facebook; 130 | 131 | // content://com.facebook.provider.PictureProvider/601234567 132 | @Override 133 | public String getUri(Uri uri) { // 轉址 - 改寫網址 134 | Picture picture = facebook.picture(uri.getPathSegments().get(0)).toBlocking().first(); 135 | 136 | return picture.data.url; 137 | } 138 | 139 | interface Facebook { 140 | // https://graph.facebook.com/{id}/picture 141 | // https://graph.facebook.com/601234567/picture 142 | @GET("/{id}/picture") 143 | Observable picture(@Path("id") String id); 144 | } 145 | 146 | static class Picture { 147 | Data data; 148 | } 149 | 150 | static class Data { 151 | // "https://fbcdn-profile-a.akamaihd.net/hprofile-ak-xpf1/v/t1.0-1/p50x50/1234567" 152 | String url; 153 | } 154 | 155 | @Override 156 | public boolean onCreate() { 157 | RestAdapter restAdapter = new RestAdapter.Builder() 158 | .setEndpoint("https://graph.facebook.com") 159 | .build(); 160 | facebook = restAdapter.create(Facebook.class); 161 | return true; 162 | } 163 | } 164 | ``` 165 | 166 | * https://github.com/yongjhih/facebook-content-provider 167 | 168 | ## See Also 169 | 170 | * Using okhttp backed for Volley - https://gist.github.com/bryanstern/4e8f1cb5a8e14c202750 171 | * https://github.com/facebook/fresco/pull/21 172 | -------------------------------------------------------------------------------- /javadoc.md: -------------------------------------------------------------------------------- 1 | # javadoc 2 | 3 | ## See Also 4 | 5 | * https://github.com/Kotlin/dokka 6 | -------------------------------------------------------------------------------- /json_to_pojo.md: -------------------------------------------------------------------------------- 1 | # json to POJO 2 | 3 | * Gson 4 | * Jackson 5 | * Instagram/ig-json-parser 6 | * LoganSquare 7 | 8 | 基本上 ig-json-parser、LoganSquare 是基於 Jackson 衍伸的,編譯時期的欄位處理,搭配 Jackson stream parsing 達成。 9 | 10 | Gson 而是執行時期作欄位處理,自然有改善的空間。 11 | 12 | LoganSquare 啟發於 Instagram/ig-json-parser 而有較好的包裝。 13 | 14 | 15 | ## LoganSquare 16 | 17 | ```java 18 | // String jsonString: 19 | // 20 | // { 21 | // name: "Andrew", 22 | // id: 1 23 | // } 24 | User user = LoganSquare.parse(jsonString, User.class); 25 | System.out.println("name: " + user.name); 26 | System.out.println("id: " + user.id); 27 | ``` 28 | 29 | ```java 30 | @JsonObject 31 | public class User { 32 | @JsonField 33 | public String name; 34 | 35 | @JsonField 36 | public int id; 37 | } 38 | ``` 39 | 40 | 先撇除物件化描述,可以先探究,為什麼 JSONObject for loop 去爬會比較慢呢? 41 | 42 | 一個初步的原因是因為先要把 json string 塞成 JSONObject ,表示已經繞完過一輪了。然後你再次繞一輪 JSONObject 就已經是兩輪了。 43 | 44 | 而 Jackson 提供的 streaming 就是正在繞的當下就會呼叫 callback 了。 45 | 46 | ## flatbuffers & protobuf 47 | 48 | They are lightweight serialized data format. 49 | 50 | The retrofit supports protobuf. 51 | 52 | .. 53 | 54 | ## See Also 55 | 56 | * http://instagram-engineering.tumblr.com/post/97147584853/json-parsing 57 | -------------------------------------------------------------------------------- /kai_yuan_ba_gua.md: -------------------------------------------------------------------------------- 1 | # 開源八卦 2 | 3 | ## [Jake Wharton](https://github.com/JakeWharton) 4 | 5 | ![](https://avatars2.githubusercontent.com/u/66577?v=3&s=160) 6 | 7 | 是個看起來不用睡覺的人 8 | 9 | 隸屬: 10 | 11 | * Square 12 | 13 | 著作: 14 | 15 | * ActionBarSherlock 16 | * AndroidViewPagerIndicator 17 | * ButterKnife 18 | * NotRxAndroid 19 | * RxAndroid 20 | * Dagger 21 | 22 | ## square/moshi 23 | 24 | moshi 是一個 json to POJO (Ojm, object json mapping) 的函式庫. 類似 Gson, jackson. 但是這個名字竟然是 Jake Wharton 他家的狗名... 25 | 26 | 27 | ![](https://pbs.twimg.com/profile_images/474396522386169856/bs16d1Gx_400x400.png) 28 | 29 | ## Chris Banes 30 | 31 | ![](https://avatars3.githubusercontent.com/u/227486?v=3&s=160) 32 | 33 | 隸屬: 34 | 35 | * Google 36 | 37 | 著作: 38 | 39 | * PullToRefresh -> support.v4.SwipeRefreshLayout 40 | * gradle-mvn-push 41 | 42 | ## Square 43 | 44 | ## [Lucas Rocha, lucasr](https://github.com/lucasr) 45 | 46 | * London, UK 47 | 48 | 隸屬: 49 | 50 | * -Mozilla- 51 | * Facebook 52 | 53 | * TwoWayView 54 | 55 | ## Jean-Baptiste Queru, JBQ 56 | 57 | ![](http://www.androidheadlines.com/wp-content/uploads/2013/09/jean-baptiste-queru.jpg) 58 | 59 | 也是個看起來不用睡覺的人 60 | 61 | * AOSP 技術首席 62 | 63 | 隸屬: 64 | 65 | * Yahoo 66 | 67 | ## Xavier Ducrohet 68 | 69 | ![](https://c2.staticflickr.com/6/5333/8904997131_e551992b9d_b.jpg) 70 | 71 | Android Build Tool team leader 72 | 73 | ## Gregory Kick 74 | 75 | ![](https://avatars1.githubusercontent.com/u/2279476?v=3&s=160) 76 | 77 | 隸屬: 78 | 79 | * Google 80 | 81 | 居住: 82 | 83 | * Chicago, IL 84 | 85 | 參與: 86 | 87 | * Dagger2 88 | 89 | ## Devoxx 90 | -------------------------------------------------------------------------------- /lambda.md: -------------------------------------------------------------------------------- 1 | # Lambda λ 2 | 3 | 一種簡化寫 callback 的表達式。 4 | 5 | Before: 6 | 7 | ```java 8 | view.setOnClickListener(new View.OnClickListener() { 9 | @Override public void onClick(View v) { 10 | println("yo"); 11 | } 12 | }); 13 | ``` 14 | 15 | After: 16 | 17 | ```java 18 | view.setOnClickListener(v -> System.out.println(v)); 19 | ``` 20 | 21 | or 22 | 23 | ```java 24 | view.setOnClickListener(System.out::println); 25 | ``` 26 | 27 | * 當 interface 只有一個 method 需要實作時,就不需要特別再說是哪個 method name 了。包含 interface name 就可以整個省略。只剩下參數名稱要寫而已,如果怕參數型別有混淆之虞可寫上型別: 28 | 29 | ```java 30 | view.setOnClickListener((View v) -> println(v)); 31 | ``` 32 | 33 | * 如果沒有參數: 34 | 35 | Before: 36 | 37 | ```java 38 | Runnable run = new Runnable() { 39 | @Override public void run() { 40 | println("yo"); 41 | } 42 | }; 43 | ``` 44 | 45 | After: 46 | 47 | ```java 48 | Runnable run = () -> println("yo"); 49 | ``` 50 | 51 | 如果你要在 Android 上使用,請參考 [gradle-retrolambda](https://github.com/evant/gradle-retrolambda) 。 52 | 53 | 54 | ## 進階:如果 abstract class 只有一個 abstract method 是否也能適用 lambda? 55 | 56 | 筆者測試過,目前是不行的。 57 | 58 | 這裡筆者有想到一招替代方案,寫個可以吃 lambda 的 abstract class creator 來幫忙生,例如: 59 | 60 | Before: 61 | 62 | ```java 63 | void save(ParseUser user) { 64 | // SaveCallback 是 abstract class 且只有一個 abstract method: "void done(ParseException e);" 65 | user.save(new SaveCallback() { 66 | @Override public void done(ParseException e) { 67 | e.printStackTrace(); 68 | } 69 | }); 70 | } 71 | ``` 72 | 73 | After: 74 | 75 | ```java 76 | void save(ParseUser user) { 77 | user.save(Callbacks.save(e -> e.printStackTrace())); 78 | } 79 | ``` 80 | 81 | 寫一個可以吃 lambda 的 Callbacks.save() 來幫忙生 abstract SaveCallback: 82 | 83 | ```java 84 | public class Callbacks { 85 | public interface ISaveCallback { 86 | void done(ParseException e); 87 | } 88 | 89 | public static SaveCallback save(ISaveCallback callback) { 90 | return new SaveCallback() { 91 | @Override public void done(ParseException e) { 92 | callback.done(e); 93 | } 94 | }; 95 | } 96 | } 97 | ``` 98 | -------------------------------------------------------------------------------- /loadermanager.md: -------------------------------------------------------------------------------- 1 | # LoaderManager 2 | 3 | * Cursor Leak: 關於 cursor 關閉問題,LoaderManader 就是在管理 lifeCycle 的,cursorLoader 就會被 loaderManager 通知來關閉 cursor。 4 | * ContentProvider: 包裝成 contentProvider 的好處在於有通用的 ContentUri 存取點(endpoint) 可用,跨 app 存取能力。所以可衡量實現的必要性。 5 | * LoaderManager + Cursor: 如果資料來源本來用就是 SQLiteDatabase ,就算沒有包裝成 ContentProvider 也勢必也有 Cursor 生命管理業務,這時可利用 LoaderManager 來管理。 6 | * Cursor without contentUri: CursorLoader 需要 ContentUri endpoint,所以在沒有實現 ContentProvider 的狀況下,可以利用 AsyncTaskLoader 來包裝 Cursor,網路上很多人寫了 SimpleCursorLoader 就是基於 AsyncTaskLoader 實現的。 7 | 8 | BTW, rxer 如果通用 loaderManager 有個 rxloader 可用: https://github.com/evant/rxloader 9 | -------------------------------------------------------------------------------- /mockito.md: -------------------------------------------------------------------------------- 1 | # Mockito 2 | 3 | 虛擬物件 4 | 5 | 用來驗證是否如預期的互動行為,例如: 6 | 7 | ```java 8 | void add(List list, String item) { 9 | list.add(item); 10 | } 11 | 12 | void testAddList() { 13 | List mockedList = mock(List.class); 14 | add(mockedList, "one"); 15 | verify(mockedList).add("one"); 16 | } 17 | ``` 18 | 19 | 也可以偽裝行為,例如: 20 | 21 | ```java 22 | LinkedList mockedList = mock(LinkedList.class); 23 | 24 | // 如果有人呼叫 get(0) ,就回傳 "first" 25 | when(mockedList.get(0)).thenReturn("first"); 26 | 27 | assertEquals("first", mockedList.get(0)); 28 | ``` 29 | 30 | 31 | ## RxParse 測項範例 32 | 33 | 測試 `ParseObservable.all(ParseQuery).subscribe(onNext, onError, onCompleted);` ,不應該先呼叫 onCompleted 才呼叫 onNext。 34 | 35 | ```java 36 | /** 37 | * ParseObservable.all(ParseQuery).subscribe(onNext, onError, onCompleted); should've onNext after onCompleted. 38 | */ 39 | @Test 40 | public void testParseObservableAllNextAfterCompleted() { 41 | // 建立三個偽裝 ParseUser 42 | ParseUser user = mock(ParseUser.class); 43 | ParseUser user2 = mock(ParseUser.class); 44 | ParseUser user3 = mock(ParseUser.class); 45 | List users = new ArrayList<>(); 46 | when(user.getObjectId()).thenReturn("" + user.hashCode()); // 偽裝呼叫 user.getObjectId() 時,回傳 hashCode 字串. 47 | users.add(user); 48 | when(user2.getObjectId()).thenReturn("" + user2.hashCode()); 49 | users.add(user2); 50 | when(user3.getObjectId()).thenReturn("" + user3.hashCode()); 51 | users.add(user3); 52 | // 偽裝 ParseQueryController ,讓 ParseQuery.findInBackground(), ParseQuery.countInBackground 使用到偽裝的 ParseQueryController 53 | ParseQueryController queryController = mock(ParseQueryController.class); 54 | ParseCorePlugins.getInstance().registerQueryController(queryController); 55 | 56 | Task> task = Task.forResult(users); 57 | // 偽裝呼叫 findAsync(ParseQuery.State, ParseUser, Task) 時,回傳已經裝好三個使用者的 task 58 | when(queryController.findAsync( 59 | any(ParseQuery.State.class), 60 | any(ParseUser.class), 61 | any(Task.class)) 62 | ).thenReturn(task); 63 | 64 | // 偽裝呼叫 countAsync(ParseQuery.State, ParseUser, Task) 時,回傳使用者個數: 3 65 | when(queryController.countAsync( 66 | any(ParseQuery.State.class), 67 | any(ParseUser.class), 68 | any(Task.class))).thenReturn(Task.forResult(users.size())); 69 | 70 | ParseQuery query = ParseQuery.getQuery(ParseUser.class); 71 | query.setUser(new ParseUser()); 72 | 73 | final AtomicBoolean completed = new AtomicBoolean(false); 74 | rx.parse.ParseObservable.all(query) 75 | //.observeOn(Schedulers.newThread()) 76 | //.subscribeOn(AndroidSchedulers.mainThread()) 77 | .subscribe(new Action1() { 78 | @Override public void call(ParseObject it) { 79 | System.out.println("onNext: " + it.getObjectId()); 80 | if (completed.get()) { // 竟然發現 onCompleted 已經被呼叫了 81 | fail("Should've onNext after onCompleted."); 82 | } 83 | } 84 | }, new Action1() { 85 | @Override public void call(Throwable e) { 86 | System.out.println("onError: " + e); 87 | } 88 | }, new Action0() { 89 | @Override public void call() { 90 | System.out.println("onCompleted"); 91 | completed.set(true); 92 | } 93 | }); 94 | 95 | try { 96 | ParseTaskUtils.wait(task); // 因為是非同步執行,需要等待 task 把結果送出,確保 callbacks 跑完。 97 | } catch (Exception e) { 98 | // do nothing 99 | } 100 | } 101 | ``` 102 | 103 | ## 安裝 104 | 105 | ```gradle 106 | dependencies { testCompile "org.mockito:mockito-core:1.+" } 107 | ``` 108 | 109 | ## See Also 110 | 111 | * Mockito - https://github.com/mockito/mockito http://mockito.org/ 112 | * EasyMock - https://github.com/easymock/easymock http://easymock.org/ 113 | * PowerMock - https://github.com/jayway/powermock 114 | -------------------------------------------------------------------------------- /multidex.md: -------------------------------------------------------------------------------- 1 | # multidex 2 | 3 | 我們知道 android app 最多 65k 數量限制。如果不幸超過,你可以透過官方的 multidex 機制來避開。 4 | 或期望 proguard 之後可以壓進 65k 內。 5 | 6 | multidex 工作原理: 7 | 8 | 透過靜態分析得知哪些需要必須靜態載入 class 放入 classes.dex 其他則依序放入 `classes{2..N}.dex` 用來動態載入,來有效避開 65k 限制。 9 | 10 | 有趣的是,靜態分析其實是透過 proguard 來分析的。 11 | 12 | 重要的依據 AndroidManifest.xml 是 app 程式進入點,Activity/Service/ContentProvider。 13 | 14 | ## 啟用 multidex 15 | 16 | ```gradle 17 | multiDexEnabled true 18 | ``` 19 | 20 | 21 | ```java 22 | public class SimpleApplication extends MultiDexApplication { 23 | // ... 24 | } 25 | ``` 26 | 27 | 28 | ## 隱藏的設定 multiDexKeepFile/multiDexKeepProguard (1.5.0 後失效?) 29 | 30 | 不過有些靜態分析是分析,像是 ActiveAndroid ORM 的 models 。啟動時就必須靜態載入。 31 | 32 | 透過 multiDexKeepFile/multiDexKeepProguard 設定,指名哪些要包在 classes.dex 來靜態載入。 33 | 34 | ```gradle 35 | multiDexKeepProguard file('multiDexKepp.txt') 36 | multiDexKeepFile file('multiDexKepp.pro') 37 | ``` 38 | 39 | multiDexKepp.txt: 40 | 41 | ``` 42 | com/bluelinelabs/logansquare/LoganSquare.class 43 | ``` 44 | 45 | multiDexKepp.pro: 46 | 47 | ```proguard 48 | -keep public class * extends com.activeandroid.Model { 49 | (); 50 | } 51 | ``` 52 | 53 | ## 啟用 proguard 54 | 55 | build.gradle: 56 | 57 | ```gradle 58 | minifyEnabled true 59 | ``` 60 | 61 | ## 計算 62 | 63 | 坊間一堆 dex counter: 64 | 65 | * 大多透過 `cat classes.dex | head -c 92 | tail -c 4 | hexdump -e '1/4 "%d"'` 取得 66 | * 透過 baksmali 比較精準 67 | 68 | 轉換 dex 流程: 69 | 70 | * apk2dex - `unzip "$apk" classes.dex` 71 | * jar2dex - `dx --dex --output="$dex" "$jar"` 72 | 73 | https://github.com/yongjhih/rc/blob/master/bin/dexize 74 | 75 | ``` 76 | dexize {apk...} 77 | ``` 78 | 79 | or 80 | 81 | ``` 82 | dexize {jar...} 83 | ``` 84 | 85 | ``` 86 | dexize {folder} 87 | ``` 88 | 89 | ## 編譯順便計算 90 | 91 | * https://github.com/KeepSafe/dexcount-gradle-plugin 92 | 93 | ```bash 94 | ./gradlew countDebugDexMethods 95 | ``` 96 | 97 | 詳細:`build/outputs/dexcount/debug.txt` 98 | 99 | * https://github.com/mutualmobile/gradle-dexinfo-plugin 100 | 101 | ```bash 102 | ./gradlew dexinfoDebug 103 | ``` 104 | 105 | ## See also 106 | 107 | * https://gist.github.com/JakeWharton/6002797 108 | * http://inloop.github.io/apk-method-count 109 | * https://medium.com/groupon-eng/android-s-multidex-slows-down-app-startup-d9f10b46770f 110 | -------------------------------------------------------------------------------- /notification.md: -------------------------------------------------------------------------------- 1 | # Notification 2 | 3 | 使用 `v4.NotificationCompat.Builder`。 4 | 5 | 6 | ## See Also 7 | 8 | * https://github.com/8tory/json2notification -------------------------------------------------------------------------------- /openlibrary.md: -------------------------------------------------------------------------------- 1 | # 開源函式庫 2 | 3 | 建立一個函式庫: 4 | 5 | ```bash 6 | android create lib-project -t 1 -k com.github.yongjhih.simplelib -p . -g -v 1.0.0 7 | ``` 8 | -------------------------------------------------------------------------------- /openpackage_jitpack_jcenter_mavencentral.md: -------------------------------------------------------------------------------- 1 | # 發布套件 2 | 3 | ## 引用套件 4 | 5 | 要把編譯好的 jar/aar 套件,上傳到套件管理中心,方便別人下載使用: 6 | 7 | ```java 8 | dependencies { 9 | compile 'com.github.yongjhih:RetroFacebook:1.0.0' 10 | } 11 | ``` 12 | 13 | 那麼你要就上到一個 maven 套件伺服器。目前知名的伺服器中心有三家: 14 | 15 | * mavenCentral 16 | * jcenter 17 | * jitpack 18 | 19 | ## mavenCentral 發布套件 20 | 21 | 由 sonatype 組織所維護, 最早的一家。 22 | 必須要開源才能被收錄。最嚴苛的一家。 23 | 24 | 申請方式,首先,你必須要有一個開源專案。 25 | 26 | 註冊 sonatype 帳號後,發 ticket ,在 ticket 內容寫你的開源網址,通常是 github repository 網址,如:https://github.com/yongjhih/RetroFacebook ,這樣你就會拿到 `com.github.yongjhih` 的 group 。以後你都可以往這個群組裡面丟 jar/aar 。 27 | 28 | 如果是公司行號要申請,就必須在 ticket 內容寫明你是域名的擁有者,例如:`com.infstory` ,這樣你才能拿到 `com.infstory` Group。 29 | 30 | mavenCentral 發布指南: 31 | 32 | * [OSSRH](http://central.sonatype.org/pages/ossrh-guide.html) 33 | 34 | ~/.gradle/gradle.properties 設定 mavenCentral 帳密與 gpg key 35 | 36 | 在你的專案安裝 https://github.com/chrisbanes/gradle-mvn-push 37 | 38 | 設定專案套件資訊 gradle.properties: 39 | 40 | ``` 41 | VERSION_NAME=0.9.2-SNAPSHOT 42 | VERSION_CODE=92 43 | GROUP=com.github.chrisbanes.actionbarpulltorefresh 44 | 45 | POM_DESCRIPTION=A modern implementation of the pull-to-refresh for Android 46 | POM_URL=https://github.com/chrisbanes/ActionBar-PullToRefresh 47 | POM_SCM_URL=https://github.com/chrisbanes/ActionBar-PullToRefresh 48 | POM_SCM_CONNECTION=scm:git@github.com:chrisbanes/ActionBar-PullToRefresh.git 49 | POM_SCM_DEV_CONNECTION=scm:git@github.com:chrisbanes/ActionBar-PullToRefresh.git 50 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 51 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 52 | POM_LICENCE_DIST=repo 53 | POM_DEVELOPER_ID=chrisbanes 54 | POM_DEVELOPER_NAME=Chris Banes 55 | ``` 56 | 57 | 上傳套件: 58 | 59 | ``` 60 | ./gradlew uploadArchives 61 | ``` 62 | 63 | 上傳後,會先進到 stage 暫存區,做掃描行為後,你進到網頁後台確定發布。不過通常半個到一個工作天才能夠真正進到 mavenCentral。 64 | 65 | ## jcenter 66 | 67 | 由 bintray 公司所維護。原則上也是需要開源。jcenter 會鏡像 mavenCentral 。而目前 jcenter 是 android studio 預設的套件中心。加上目前由於網頁設計的比較便民,所以大多已經改由 jcenter 發布了。當然很多其他系統還是只有 mavenCentral 所以 jcenter 也提供同步到 mavenCentral ,不過你要給它 OSSRH 的帳密就是了,當然 jcenter 也表明不會儲存你的帳密。 68 | 69 | 安裝 https://github.com/novoda/bintray-release 70 | 71 | 上傳套件: 72 | 73 | ``` 74 | ./gradlew bintrayUpload 75 | ``` 76 | 77 | 去 bintray.com 後台一鍵申請發布到 jcenter ,通常一天內就會審核通過。(未來這部份應該透過上傳程式就直接提出申請,這樣省事多了) 78 | 79 | ## jitpack 80 | 81 | 大約是 2015 年初的時候成立的。最大的特點是,直接支援 github/bitbucket repository 。所以不用特別申請套件中心帳號。因為不是你去放套件,而是它自己來拉你的源碼來編譯 jar/aar。 82 | 83 | ### jitpack 引用套件 84 | 85 | 引用方 build.gradle: 86 | 87 | ```gradle 88 | repositories { 89 | // ... 90 | maven { url "https://jitpack.io" } 91 | } 92 | 93 | dependencies { 94 | compile 'com.github.{user}:{repo}:{version}' 95 | } 96 | ``` 97 | 98 | ### jitpack 發布套件 99 | 100 | 因為是引用時編譯,不需要自己編譯上傳發布,只要安裝配置好基本 script 提交到 github repo ,就沒事了。 101 | 102 | 在 module 內 build.gradle 加入: 103 | 104 | ```gradle 105 | buildscript { 106 | repositories { 107 | jcenter() 108 | } 109 | 110 | dependencies { 111 | // ... 112 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' 113 | } 114 | } 115 | 116 | apply plugin: 'com.github.dcendents.android-maven' 117 | apply from: 'android-javadoc.gradle' 118 | ``` 119 | 120 | 以及新增 [android-javadoc.gradle](https://gist.github.com/yongjhih/3f1ed93c80e0b248cf55): 121 | 122 | ```gradle 123 | task sourcesJar(type: Jar) { 124 | from android.sourceSets.main.java.srcDirs 125 | classifier = 'sources' 126 | } 127 | 128 | task javadoc(type: Javadoc) { 129 | failOnError false 130 | source = android.sourceSets.main.java.sourceFiles 131 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 132 | } 133 | 134 | task javadocJar(type: Jar, dependsOn: javadoc) { 135 | classifier = 'javadoc' 136 | from javadoc.destinationDir 137 | } 138 | 139 | artifacts { 140 | archives sourcesJar 141 | archives javadocJar 142 | } 143 | ``` 144 | 145 | 如果你引用的了太多 jitpack 上面的套件,會需要很長的時間等待 jitpack 的編譯時間。故筆者作為一個函式庫貢獻者,盡可能還是採取 jitpack 與 jcenter 並行。甚者,如有閒暇會讓 jcenter 同步到上游 mavenCentral 。 146 | 147 | *p.s. jitpack 未來或許營利除了私有套件之外,可能可以提供付費服務,如報表等。javadoc 生成塞廣告。結合 travis-ci, cycle-ci 。同步 mavenCentral/jcenter* 148 | 149 | ## 線上 javadoc 150 | 151 | * javadoc.io for maven central, `http://javadoc.io/doc/{GROUP}{.SUBGROUP}/{ARTIFACT}/{VERSION}` 152 | * jitpack, `https://jitpack.io/{GROUP}{/SUBGROUP}/{ARTIFACT}/{VERSION}/javadoc/` 153 | 154 | ## ref. 155 | 156 | * https://github.com/chrisbanes/gradle-mvn-push 157 | * https://github.com/novoda/bintray-release 158 | -------------------------------------------------------------------------------- /orm_-_activeandroid,_dbflow,_ollie.md: -------------------------------------------------------------------------------- 1 | # Orm - ActiveAndroid, DBFlow, Ollie 2 | 3 | ORM, Object Relational Mapping, 物件關聯映射. 筆者將之譯為「關聯資料物件化」。 4 | 5 | ```sql 6 | CREATE TABLE User (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT); 7 | ``` 8 | 9 | ```java 10 | class User { 11 | public Long id; 12 | public String name; 13 | } 14 | 15 | // set 16 | User.create().name("Andrew Chen").save(); 17 | 18 | // get 19 | Cursor cursor = MyDb.query("Select * from User where name = ?", "Andrew Chen"); 20 | List users = Orm.loadFromCursor(User.class, cursor); 21 | User andrew = users.get(0); 22 | ``` 23 | 24 | 以往的 Orm library 其實有些瓶頸, 25 | 例如:query 回來的都是 `List` ,這象徵著 query 萬一很多筆,Orm 必須全部繞完把每個都組成 Model ,真正有用過會發現會非常的緩慢,就算用 Transaction 包起來加速,也無法有效改善。這部份我們團隊寫了一個 CursorList 來作 Lazy 是等用到才去組,效果很好。 26 | 27 | 這個概念 sprinkles Orm library 也有發現,所以做了跟我們團隊一樣的事情。(DBFlow 有採納) 28 | 29 | sprinkles 還有作自動升級程式也就是在 Model 新增欄位不用自己寫 alter table 。這點 DBFlow 應該也有採納。 30 | 31 | 目前我們是 ActivieAndroid + CursorList + EventBus 來作 Observing model,是由於開發時沒有 sprinkles 與 DBFlow,到現在已經沒法方便轉過去了。 32 | 33 | 另外 DBFlow 很巧妙的使用 `@interface Table RetentionPolicy.SOURCE` 編譯時期產生一個承載欄位名稱的子類別, 所以可以用 `ProfileModel$Table.DISPLAYNAME` 取出 "displayname" 欄位名稱,來有效減少 hard code 。 34 | 35 | ```java 36 | ProfileModel extends BaseModel { 37 | @Column String displayname; 38 | } 39 | ``` 40 | 41 | 緩慢確實有一部份是 reflection 所造成,而 reflection 緩慢的主因在於解析 annotation 。所以通常我們會把 NoteModel.class 所有的 column attribute 都存到一個 annotations cache map 避免重複解析 annotation 。所以只會慢在建立 annotations cache map 那第一次而已,Ollie ModelAdapter 主要解決的首重,就是這個廣泛存在於 Orm lib 初始化過慢問題。 42 | 43 | ```java 44 | List notes = new Select().from(Note.class).execute(); // huge notes 45 | ``` 46 | 47 | 在萬筆資料的存取時,造成的緩慢是在於建立萬筆 Model 的累計時間,而不在於 reflection annotations (already in cache)。 48 | 49 | 1. 試想為什麼 SQLiteDatabase 存取時,回傳的不是 List 而是 Cursor。 50 | 2. 試想 cursor 所有資料已經都從 DB 拉出來了?還是當跑 cursor.next(), cursor.get*() 才去拉資料?(DB 應該只是快速扼要的把數量與索引準備好,就馬上拋回 cursor ,當 cursor.next() 才把該筆資料找出來 ,cursor.getString() 才動用 io 去拉資料。) 51 | 3. 再試想另一個:為什麼需要 CursorAdapter 而不是 loop cursor 塞進 ArrayAdapter 就好。 `// while (cursor.moveToNext()) notes.add(Note.load(cursor)); new ArrayAdapter(notes);` 52 | 53 | 問題都指向同一個 -> 因為大量的 io (loop cursor)以及 建立大量的物件(Note.load(cursor)) 累計起來過慢。這些資料通常運算的部份已經在 query 條件過程中結束,僅為了拿出來顯示,但是這麼大量的資料全都要顯示出來的機會,其實很低,就這個狀況,可以 lazy 概念緩解,指到哪拿到哪。SQLiteCursor 基本上就是一種 lazy 的存在。 54 | 55 | 整理問題與解法: 56 | 57 | 1. 解析 annotation -> cache 58 | 2. loop cursor -> lazy 59 | 3. load from cursor -> lazy 60 | 61 | 量測方法: 62 | 63 | 1. 把 `com.activeandroid.util.SQLiteUtils.processCursor() do {} while (cursor.moveToNext())` 做空,單純測量 loop cursor 的耗時 64 | 2. 量測 `Model.loadFromCursor()` 65 | 66 | ## 名詞解釋 67 | 68 | DTO, Data Transfer Object, 資料傳輸物件。「參數物件化」 69 | 70 | ## See Also 71 | 72 | * DBFlow 73 | * ActiveAndroid 74 | * Sprinkles 75 | * Ollie 76 | * Realm 77 | * greenDAO 78 | * OrmLite 79 | * Sugar ORM 80 | * StorIO 81 | * RxCupboard 82 | * http://www.raizlabs.com/dev/2015/02/go-dbflow-fastest-android-orm-database-library/ 83 | * https://github.com/Raizlabs/AndroidDatabaseLibraryComparison 84 | * SqlBrite 85 | * couchbaselite 86 | -------------------------------------------------------------------------------- /parse.md: -------------------------------------------------------------------------------- 1 | # Parse 2 | 3 | 一種 BaaS 。 4 | 5 | ## RxParse 6 | 7 | * https://github.com/yongjhih/RxParse 8 | 9 | 「找出我留言過的貼文」: 10 | 11 | 原本的 FindCallback 寫法: 12 | 13 | ```java 14 | ParseComment.getQuery().whereEqualTo("from", ParseUser.getCurrentUser()).findInBackground(new FindCallback { 15 | @Override 16 | public done(List comments, ParseException e) { 17 | if (e != null) return; 18 | 19 | ParsePost.getQuery().whereContainedIn("comments", comments).findInBackground(new FindCallback() { 20 | @Override 21 | public done(List posts, ParseException e2) { 22 | if (e2 != null) return; 23 | 24 | // ... 25 | } 26 | }); 27 | } 28 | }); 29 | ``` 30 | 31 | FindCallback 拆解寫法: 32 | 33 | ```java 34 | getMyCommentedPosts(new FindCallback() { 35 | @Override 36 | public done(List posts, ParseException e) { 37 | if (e != null) return; 38 | 39 | // ... 40 | } 41 | }); 42 | 43 | public static void getMyCommentedPosts(FindCallback findCallback) { 44 | getMyComments(new FindCallback { 45 | @Override 46 | public done(List comments, ParseException e) { 47 | if (e != null) { 48 | findCallback.done(null, e); 49 | return; 50 | } 51 | 52 | ParsePost.getQuery().whereContainedIn("comments", comments).findInBackground(findCallback); 53 | } 54 | }); 55 | } 56 | 57 | public static void getMyComments(FindCallback findCallback) { 58 | ParseComment.getQuery().whereEqualTo("from", ParseUser.getCurrentUser()).findInBackground(findCallback); 59 | } 60 | 61 | ``` 62 | 63 | 改成 Bolts Promise 寫法: 64 | 65 | ```java 66 | ParseComment.getQuery().whereEqualTo("from", ParseUser.getCurrentUser()).findInBackground() 67 | .continueWithTask(new Continuation, Task>>() { 68 | public Task> then(Task> task) throws Exception { 69 | if (task.isFaulted()) return null; 70 | 71 | return ParsePost.getQuery().whereContainedIn("comments", task.getResult()).findInBackground(); 72 | } 73 | }).onSuccess(new Continuation>, Void>() { 74 | public Void then(Task> task) throws Exception { 75 | List posts = task.getResult(); 76 | 77 | // ... 78 | return null; 79 | } 80 | }); 81 | ``` 82 | 83 | Bolts Promise 拆解寫法: 84 | 85 | ```java 86 | getMyCommentedPostsTask().onSuccess(new Continuation>, Void>() { 87 | public Void then(Task> task) throws Exception { 88 | List posts = task.getResult(); 89 | 90 | // ... 91 | return null; 92 | } 93 | }); 94 | 95 | public static Task> getMyCommentedPostsTask() { 96 | return getMyCommentsTask().continueWithTask(new Continuation, Task>>() { 97 | public Task> then(Task> task) throws Exception { 98 | if (task.isFaulted()) return null; 99 | 100 | return ParsePost.getQuery().whereContainedIn("comments", task.getResult()).findInBackground(); 101 | } 102 | }); 103 | } 104 | 105 | public static Task> getMyCommentsTask() { 106 | return ParseComment.getQuery().whereEqualTo("from", ParseUser.getCurrentUser()).findInBackground(); 107 | } 108 | 109 | ``` 110 | 111 | RxParse 寫法: 112 | 113 | ```java 114 | ParseObservable.find(ParseComment.getQuery().whereEqualTo("from", ParseUser.getCurrentUser())) 115 | .toList() 116 | .flatMap(comments -> ParseObservable.find(ParsePost.getQuery().whereContainedIn("comments", comments))) 117 | .subscribe(posts -> {}); 118 | ``` 119 | 120 | RxParse 拆解寫法: 121 | 122 | ```java 123 | getMyCommentedPosts().subscribe(posts -> {}); 124 | 125 | public static Observable getMyCommentedPosts() { 126 | return getMyComments().toList().flatMap(comments -> ParseObservable.find(ParsePost.getQuery().whereContainedIn("comments", comments))); 127 | } 128 | 129 | public static Observable getMyComments() { 130 | return ParseObservable.find(ParseComment.getQuery().whereEqualTo("from", ParseUser.getCurrentUser())); 131 | } 132 | 133 | ``` 134 | 135 | ## RxParse 測項 136 | 137 | ```java 138 | @Test 139 | public void testParseObservableAllNextAfterCompleted() { 140 | ParseUser user = mock(ParseUser.class); 141 | ParseUser user2 = mock(ParseUser.class); 142 | ParseUser user3 = mock(ParseUser.class); 143 | List users = new ArrayList<>(); 144 | when(user.getObjectId()).thenReturn("" + user.hashCode()); 145 | users.add(user); 146 | when(user2.getObjectId()).thenReturn("" + user2.hashCode()); 147 | users.add(user2); 148 | when(user3.getObjectId()).thenReturn("" + user3.hashCode()); 149 | users.add(user3); 150 | ParseQueryController queryController = mock(ParseQueryController.class); 151 | ParseCorePlugins.getInstance().registerQueryController(queryController); 152 | 153 | Task> task = Task.forResult(users); 154 | when(queryController.findAsync( 155 | any(ParseQuery.State.class), 156 | any(ParseUser.class), 157 | any(Task.class)) 158 | ).thenReturn(task); 159 | when(queryController.countAsync( 160 | any(ParseQuery.State.class), 161 | any(ParseUser.class), 162 | any(Task.class))).thenReturn(Task.forResult(users.size())); 163 | 164 | ParseQuery query = ParseQuery.getQuery(ParseUser.class); 165 | query.setUser(new ParseUser()); 166 | 167 | final AtomicBoolean completed = new AtomicBoolean(false); 168 | rx.parse.ParseObservable.all(query) 169 | //.observeOn(Schedulers.newThread()) 170 | //.subscribeOn(AndroidSchedulers.mainThread()) 171 | .subscribe(new Action1() { 172 | @Override public void call(ParseObject it) { 173 | System.out.println("onNext: " + it.getObjectId()); 174 | if (completed.get()) { 175 | fail("Should've onNext after completed."); 176 | } 177 | } 178 | }, new Action1() { 179 | @Override public void call(Throwable e) { 180 | System.out.println("onError: " + e); 181 | } 182 | }, new Action0() { 183 | @Override public void call() { 184 | System.out.println("onCompleted"); 185 | completed.set(true); 186 | } 187 | }); 188 | 189 | try { 190 | ParseTaskUtils.wait(task); 191 | } catch (Exception e) { 192 | // do nothing 193 | } 194 | } 195 | 196 | @Test 197 | public void testParseObservableFindNextAfterCompleted() { 198 | ParseUser user = mock(ParseUser.class); 199 | ParseUser user2 = mock(ParseUser.class); 200 | ParseUser user3 = mock(ParseUser.class); 201 | List users = new ArrayList<>(); 202 | users.add(user); 203 | users.add(user2); 204 | users.add(user3); 205 | ParseQueryController queryController = mock(ParseQueryController.class); 206 | ParseCorePlugins.getInstance().registerQueryController(queryController); 207 | 208 | Task> task = Task.forResult(users); 209 | when(queryController.findAsync( 210 | any(ParseQuery.State.class), 211 | any(ParseUser.class), 212 | any(Task.class)) 213 | ).thenReturn(task); 214 | 215 | ParseQuery query = ParseQuery.getQuery(ParseUser.class); 216 | query.setUser(new ParseUser()); 217 | 218 | final AtomicBoolean completed = new AtomicBoolean(false); 219 | rx.parse.ParseObservable.find(query) 220 | //.observeOn(Schedulers.newThread()) 221 | //.subscribeOn(AndroidSchedulers.mainThread()) 222 | .subscribe(new Action1() { 223 | @Override public void call(ParseObject it) { 224 | System.out.println("onNext: " + it); 225 | if (completed.get()) { 226 | fail("Should've onNext after completed."); 227 | } 228 | } 229 | }, new Action1() { 230 | @Override public void call(Throwable e) { 231 | System.out.println("onError: " + e); 232 | } 233 | }, new Action0() { 234 | @Override public void call() { 235 | System.out.println("onCompleted"); 236 | completed.set(true); 237 | } 238 | }); 239 | 240 | try { 241 | ParseTaskUtils.wait(task); 242 | } catch (Exception e) { 243 | // do nothing 244 | } 245 | } 246 | ``` 247 | 248 | * https://github.com/yongjhih/RxParse/blob/master/rxparse/src/test/java/com/parse/ParseObservableTest.java 249 | 250 | ## Cloud Coding 251 | 252 | Before, cloud code 原本的寫法: 253 | 254 | ```js 255 | Parse.Cloud.define("signInWithWeibo", function (request, response)) { // 註冊 RPC 名稱 256 | //console.log(request.user + request.params.accessToken); // 取參數,對應 android 手機端 ParseCloud.callFunctionInBackground("signInWithWeibo", Map); 257 | // if (where) response.success(obj); // 回傳資料 258 | // else response.error(error); // 回報錯誤 259 | } 260 | ``` 261 | 262 | After, 1. 改善註冊 RPC 的方法: 263 | 264 | ```js 265 | defineCloud(signInWithWeibo); 266 | 267 | function signInWithWeibo(request, response) { 268 | // ... 269 | } 270 | 271 | function defineCloud(func) { 272 | Parse.Cloud.define(func.name, func); // func.name 可以取得 func 的函式名稱 273 | } 274 | ``` 275 | 276 | After, 2. 將 response 機制隱藏,轉成對應的 Promise : 277 | 278 | ```js 279 | function promiseResponse(promise, response) { 280 | promise.then(function (o) { 281 | response.success(o); 282 | }, function (error) { 283 | response.error(error); 284 | }) 285 | } 286 | 287 | /** 288 | * Returns the session token of available parse user via weibo access token within `request.params.accessToken`. 289 | * 290 | * @param {Object} request Require request.params.accessToken 291 | * @param {Object} response 292 | * @returns {String} sessionToken 293 | */ 294 | function signInWithWeibo(request, response) { 295 | promiseResponse(signInWithWeiboPromise(request.user, request.params.accessToken, request.params.expiresTime), response); 296 | } 297 | 298 | /** 299 | * Returns the session token of available parse user via weibo access token. 300 | * 301 | * @param {Parse.User} user 302 | * @param {String} accessToken 303 | * @param {Number} expiresTime 304 | * @returns {Promise} sessionToken 305 | */ 306 | function signInWithWeiboPromise(user, accessToken, expiresTime) { 307 | // ... 308 | } 309 | ``` 310 | 311 | ## Parse.Cloud.httpRequest 312 | 313 | 回傳 `{Promise}` ,所可以接龍: 314 | 315 | ```js 316 | /** @returns {Promise} email */ 317 | function getEmail(accessToken) { 318 | // GET https://api.weibo.com/2/account/profile/email.json?access_token={access_token} 319 | // 這裡嚴格分離的 params 方式, 好處是未來改成 POST 也統一寫法 320 | return Parse.Cloud.httpRequest({ 321 | url: "https://api.weibo.com/2/account/profile/email.json", 322 | params: { 323 | access_token: accessToken 324 | } 325 | }).then(function (httpResponse) { 326 | return JSON.parse(httpResponse.text)[0].email; // [ { email: "abc@example.com" } ] 327 | }); 328 | } 329 | ``` 330 | 331 | ## Promise 332 | 333 | * `Parse.Promise.as("Hello")` 334 | 335 | ```js 336 | Parse.Promise.as("Hello").then(function (hello) { 337 | console.log(hello); 338 | }); 339 | ``` 340 | 341 | * `Parse.Promise.when(helloPromise, worldPromise)` 342 | 343 | ```js 344 | var helloPromise = Parse.Promise.as("Hello"); 345 | var worldPromise = Parse.Promise.as(", world!"); 346 | Parse.Promise.when(helloPromise, worldPromise).then(function (hello, world) { 347 | console.log(hello + world); 348 | }); 349 | ``` 350 | 351 | flat and zip: 352 | 353 | ```js 354 | var helloPromise = Parse.Promise.as("Hello"); 355 | var worldPromise = Parse.Promise.as(", world!"); 356 | helloPromise.then(function (hello) { 357 | return Parse.Promise.when(Parse.Promise.as(hello), worldPromise); 358 | }).then(function (hello, world) { 359 | console.log(hello + world); 360 | }); 361 | ``` 362 | 363 | Error handling: 364 | 365 | ```js 366 | /** 367 | * Returns email. 368 | * 369 | * @param {String} accessToken 370 | * @returns {Promise} email 371 | */ 372 | function getEmailAlternative(accessToken) { 373 | return getEmail(accessToken).then(function (email) { 374 | if (!email) return Parse.Promise.error("Invalid email"); 375 | 376 | return Parse.Promise.as(email); 377 | }, function (error) { 378 | return getUid(accessToken).then(function (uid) { 379 | return Parse.Promise.as(uid + "@weibo.com"); 380 | }); 381 | }); 382 | } 383 | 384 | /** 385 | * Returns email 386 | * 387 | * @param {String} accessToken 388 | * @returns {Promise} email 389 | */ 390 | function getEmail(accessToken) { 391 | return Parse.Cloud.httpRequest({ 392 | url: "https://api.weibo.com/2/account/profile/email.json", 393 | params: { 394 | access_token: accessToken 395 | } 396 | }).then(function (httpResponse) { 397 | return JSON.parse(httpResponse.text)[0].email; // [ { email: "abc@example.com" } ] 398 | }); 399 | }; 400 | 401 | /** 402 | * Returns uid. 403 | * 404 | * @param {String} accessToken 405 | * @returns {Promise} uid 406 | */ 407 | function getUid(accessToken) { 408 | return Parse.Cloud.httpRequest({ 409 | url: "https://api.weibo.com/2/account/get_uid.json", 410 | params: { 411 | access_token: accessToken 412 | } 413 | }).then(function (httpResponse) { 414 | return JSON.parse(httpResponse.text).uid; // { uid: 5647447265 } 415 | }); 416 | } 417 | ``` 418 | 419 | ref. 420 | 421 | * https://gist.github.com/yongjhih/e196e01fc7da9c03ce7e 422 | -------------------------------------------------------------------------------- /proguard.md: -------------------------------------------------------------------------------- 1 | # Proguard 2 | 3 | build.gradle: 4 | 5 | ```gradle 6 | proguardFiles gson.pro 7 | proguardFiles joda-time.pro 8 | ... 9 | ``` 10 | 11 | 或者 proguard-rules.pro: 12 | 13 | ```gradle 14 | -include gson.pro 15 | -include joda-time.pro 16 | ... 17 | ``` 18 | 19 | ## 啟用 proguard 20 | 21 | build.gradle: 22 | 23 | ```gradle 24 | minifyEnabled true 25 | ``` 26 | 27 | ## 一行搞定所有知名函式庫 proguard 設定 28 | 29 | build.gradle: 30 | 31 | ```gradle 32 | dependencies { 33 | compile 'com.infstory:proguard-snippets:1.0.0' 34 | } 35 | ``` 36 | 37 | 就可以搞定所有知名的函式庫,如:gson、Joda-Time、RxJava。 38 | 39 | proguard-snippets 它的作用是: 40 | 41 | * 蒐集所有知名函式庫的 *.pro 42 | * 利用 aar 可以包裝 *.pro 的特性 43 | * 利用 consumerProguardFiles 幫你來套用 *.pro 44 | 45 | ```gradle 46 | consumerProguardFiles fileTree(dir: ‘libraries’, include: ‘*.pro’) 47 | ``` 48 | 49 | * 源碼:https://github.com/yongjhih/proguard-snippets 50 | 51 | 目前支援的知名函式庫: 52 | 53 | * ACRA 4.5.0 54 | * ActionBarSherlock 4.4.0 55 | * ActiveAndroid 56 | * Amazon Web Services 1.6.x / 1.7.x 57 | * Amazon Web Services 2.1.x 58 | * AndroidAnnotations 59 | * android-gif-drawable 60 | * Apache Avro 61 | * Butterknife 5.1.2 62 | * Crashlytics 1.+ 63 | * Crittercism 64 | * EventBus 2.0.2 65 | * Facebook 3.2.0 66 | * Facebook Conceal 67 | * Flurry 3.4.0 68 | * Google Analytics 3.0+ 69 | * Google Guava 70 | * Google Play Services 4.3.23 71 | * GreenDao 1.3.x 72 | * GSON 2.2.4 73 | * Jackson 2.x 74 | * Joda-Convert 1.6 75 | * Joda-Time 2.3 76 | * New Relic 77 | * Parse 78 | * Realm 79 | * RxJava 0.21 80 | * Support Library v7 81 | * Sqlite 82 | * Square OkHttp 83 | * Square Okio 84 | * Square Otto 85 | * Square Picasso 86 | * Square Retrofit 87 | * Square Wire 88 | * Icepick 89 | -------------------------------------------------------------------------------- /react_native.md: -------------------------------------------------------------------------------- 1 | # React Native 2 | 3 | * https://github.com/8tory/json2notification 4 | * https://github.com/Avocarrot/json2view 5 | * http://nativescript.org 6 | 7 | 目標都是 remote 顯示 native 畫面。 8 | 9 | React 基本上是一種 View 的存在。 10 | 11 | 先無論描述 View 的語言簡潔與否,以遠端更新 View 的能力加上強健的生態,似乎不容小覷。 12 | 13 | ## See also 14 | 15 | * https://react.parts/native 16 | 17 | -------------------------------------------------------------------------------- /recyclerview_twowayview.md: -------------------------------------------------------------------------------- 1 | # RecyclerView 簡介 2 | 3 | 基本使用方法: 4 | 5 | Before: 6 | 7 | ```java 8 | @InjectView(R.id.icons) 9 | RecyclerView icons; 10 | 11 | public void onCreate(Bundle savedInstanceState) { 12 | super.onCreate(savedInstanceState); 13 | 14 | final List list = new ArrayList<>(Arrays.asList("http://example.com/a.png"))); 15 | 16 | RecyclerAdapter listAdapter = new RecyclerAdapter<>() { 17 | @Override 18 | public IconViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 19 | return new IconViewHolder(LayoutInflater.from(context).inflate(R.layout.item_icon, parent, false))); 20 | } 21 | 22 | @Override 23 | public void onBindViewHolder(IconViewHolder viewHolder, int position) { 24 | viewHolder.icon.setImageURI(Uri.parse(list.get(position))); 25 | } 26 | } 27 | 28 | icons.setLayoutManager(new LinearLayoutManager(activity)); 29 | icons.setAdapter(listAdapter); 30 | 31 | list.add("http://example.com/b.png"); 32 | listAdapter.notifyDataSetChanged(); 33 | } 34 | ``` 35 | 36 | ```java 37 | public class IconViewHolder extends BindViewHolder { 38 | @InjectView(R.id.icon) 39 | public SimpleDraweeView icon; 40 | 41 | public IconViewHolder(View itemView) { 42 | super(itemView); 43 | ButterKnife.inject(this, itemView); 44 | } 45 | } 46 | ``` 47 | 有資料外漏的情形。 48 | 49 | 改用筆者簡單寫的 `ListRecyclerAdapter` (https://github.com/yongjhih/recyclerlist) 50 | 51 | https://gist.github.com/yongjhih/d8db8c69190293098eec 52 | 53 | After: 54 | 55 | ```java 56 | @InjectView(R.id.icons) 57 | public RecyclerView icons; 58 | 59 | public void onCreate(Bundle savedInstanceState) { 60 | super.onCreate(savedInstanceState); 61 | 62 | ListRecyclerAdapter listAdapter = ListRecyclerAdapter.create(); 63 | listAdapter.getList().add("http://example.com/a.png"); 64 | 65 | listAdapter.createViewHolder((parent, viewType) -> new IconViewHolder(LayoutInflater.from(context).inflate(R.layout.item_icon, parent, false))); 66 | 67 | icons.setLayoutManager(new LinearLayoutManager(activity)); 68 | icons.setAdapter(listAdapter); 69 | 70 | listAdapter.getList().add("http://example.com/b.png"); 71 | listAdapter.notifyDataSetChanged(); // TODO hook List.add(), List.addAll(), etc. modifitable operations 72 | } 73 | ``` 74 | 75 | IconViewHolder: 76 | 77 | ```java 78 | public class IconViewHolder extends BindViewHolder { 79 | @InjectView(R.id.icon) 80 | public SimpleDraweeView icon; 81 | 82 | public IconViewHolder(View itemView) { 83 | super(itemView); 84 | ButterKnife.inject(this, itemView); 85 | } 86 | 87 | @Override 88 | public void onBind(int position, String item) { 89 | icon.setImageURI(Uri.parse(item)); 90 | } 91 | } 92 | ``` 93 | 94 | 以 Avatar 頭像為例: 95 | 96 | 97 | ```java 98 | // 1. listAdapter.createViewHolder(): 決定使用哪種 layout + viewHolder 99 | // 2. ViewModel.builder(): 以及如何把原始資料降階至流通性資料格式 100 | // 不需要瞭解具備 View 相關設定知識,如圖片顯示現在是用 SimpleDraweeView.setImageURI(Uri) 如何設定,還是用 ImageLoader.getInstance().displayImage(ImageView iv, String url) 該如何使用之類的知識。只要乖乖把資料按照流通資料介面填好就好。 101 | // 如果畫面要微調,直接複製 layout 調整風格。 102 | public void onCreate(Bundle savedInstanceState) { 103 | super.onCreate(savedInstanceState); 104 | ... 105 | 106 | //ListRecyclerAdapter listAdapter = ListRecyclerAdapter.create(); 107 | //listAdapter.getList().add("http://example.com/a.png"); 108 | ListRecyclerAdapter listAdapter = ListRecyclerAdapter.create(); 109 | 110 | listAdapter.createViewHolder((parent, viewType) -> new AvatarViewHolder(LayoutInflater.from(context).inflate(R.layout.item_icon, parent, false))); 111 | 112 | listAdapter.getList().add(AvatarViewModel.builder().icon("http://example.com/a.png").name("Andrew Chen").updatedAt(new Date().getTime()).build()); 113 | for (User user : getUsers()) { // 新增其他使用者 114 | listAdapter.getList().add(AvatarViewModel.builder().icon(user.getPicture()).name(user.getDisplayName()).updatedAt(user.getUpdatedAt().getTime()).build()); 115 | } 116 | // via RxJava 117 | // listAdapter.getList().addAll(Observable.from(getUsers()).map(user -> AvatarViewModel.builder().icon(user.getPicture()).name(user.getDisplayName()).build()).updatedAt(user.getUpdatedAt().getTime()).toList().toBlocking().single()); 118 | 119 | ... 120 | } 121 | ``` 122 | 123 | ```java 124 | // 只需具備 View 相關知識,設定流通性資料 125 | // 不需要瞭解原始資料來源格式(不管是從 Facebook/Parse 還是 local sqlite 或 local SharedPreferences) ,只瞭解流通性基本資料格式即可。 126 | // 其中一個優點是當來源資料改變成其他格式,你依然可以沿用此 ViewHolder 127 | public class AvatarViewHolder extends BindViewHolder { 128 | @InjectView(R.id.icon) 129 | public SimpleDraweeView icon; 130 | @InjectView(R.id.text1) 131 | public TextView name; 132 | @InjectView(R.id.updatedAt) 133 | public RelativeTimeTextView updatedAt; 134 | 135 | public AvatarViewHolder(View view) { 136 | super(view); 137 | ButterKnife.inject(this, view); 138 | } 139 | 140 | @Override 141 | public void onBind(int position, AvatarViewModel item) { 142 | //icon.setImageURI(Uri.parse(item)); 143 | icon.setImageURI(Uri.parse(item.icon())); 144 | name.setText(item.name()); 145 | updatedAt.setReferenceTime(item.updatedAt()); 146 | } 147 | } 148 | ``` 149 | 150 | ```java 151 | // 提供流通性基本資料的介面 152 | @AutoParcel 153 | public abstract class AvatarViewModel implements Parcelable { 154 | public abstract String icon(); 155 | public abstract String name(); 156 | public abstract Long updatedAt(); 157 | 158 | public static Builder builder() { 159 | return new AutoParcel_AvatarViewModel.Builder(); 160 | } 161 | 162 | @AutoParcel.Builder 163 | public interface Builder { 164 | public Builder icon(String x); 165 | public Builder name(String x); 166 | public Builder updatedAt(Long x); 167 | public AvatarViewModel build(); 168 | } 169 | } 170 | ``` 171 | 172 | * http://stackoverflow.com/questions/26649406/nested-recycler-view-height-doesnt-wrap-its-content/28510031 173 | -------------------------------------------------------------------------------- /resources.md: -------------------------------------------------------------------------------- 1 | # 資源 2 | 3 | ## Development 4 | 5 | * http://androidweekly.net/ 6 | * http://android-arsenal.com/ (http://android-arsenal.herokuapp.com/) 7 | * https://github.com/trending?l=java 8 | * https://github.com/wasabeef/awesome-android-ui / https://github.com/wasabeef/awesome-android-libraries 9 | * http://developer.android.com/develop 10 | 11 | 12 | * http://androidlibs.org/ 13 | * https://android-libs.com/ 14 | 15 | ## UI/UX 16 | 17 | * https://dribbble.com/ 18 | * http://androidniceties.tumblr.com/ 19 | * https://www.materialup.com/ 20 | * http://developer.android.com/design/ 21 | 22 | ## Forum 23 | 24 | * https://www.facebook.com/groups/270034869726161/ 25 | * https://www.facebook.com/groups/AKDGroup/ 26 | 27 | ## Documentation 28 | 29 | * https://github.com/codepath/android_guides/wiki 30 | 31 | ## Tools 32 | 33 | * http://romannurik.github.io/AndroidAssetStudio/ (forks: http://jgilfelt.github.io/AndroidAssetStudio) 34 | * http://petrnohejl.github.io/Android-Cheatsheet-For-Graphic-Designers/ 35 | * http://angrytools.com/android/button 36 | * http://inloop.github.io/shadow4android/ 37 | 38 | ## apk deploy for live demo 39 | 40 | * appetize.io 41 | -------------------------------------------------------------------------------- /retrofit.md: -------------------------------------------------------------------------------- 1 | # Retrofit 1 2 | 3 | ps. We adapted retrofit since 2014, 1.5.0+. 4 | 5 | rest api 直接對應成 java interface ,且搭配 Json Mapper 直接轉換。 6 | 7 | 一個類似於 JAX-RS/JSR-311) 的實現,基於[一些理由](https://github.com/square/retrofit/issues/573)並沒有完全按照 JAX-RS 實現。 8 | 9 | Interface: 10 | 11 | ```java 12 | interface GitHub { 13 | @GET("/repos/{owner}/{repo}/contributors") 14 | Observable> contributors( 15 | @Path("owner") String owner, 16 | @Path("repo") String repo); 17 | 18 | @GET("/users/{user}") 19 | Observable user( 20 | @Path("user") String user); 21 | 22 | @GET("/users/{user}/starred") 23 | Observable> starred( 24 | @Path("user") String user); 25 | } 26 | ``` 27 | 28 | Usage: 29 | 30 | ```java 31 | RestAdapter restAdapter = new RestAdapter.Builder() 32 | .setEndpoint("https://api.github.com") 33 | .build(); 34 | 35 | GitHub github = restAdapter.create(GitHub.class); 36 | 37 | github.contributors("ReactiveX", "RxJava") 38 | .flatMap(list -> Observable.from(list)) 39 | .forEach(c -> System.out.println(c.login + "\t" + c.contributions)); 40 | ``` 41 | 42 | Models: 43 | 44 | ```java 45 | static class Contributor { 46 | String login; 47 | int contributions; 48 | } 49 | 50 | static class User { 51 | String name; 52 | } 53 | 54 | static class Repo { 55 | String full_name; 56 | } 57 | ``` 58 | 59 | ## Retrofit 1 網路處理 60 | 61 | retrofit 內部運作也是使用 `Request` 概念來運行。 62 | 63 | Retrofit 很顯然的,希望讓開發者專注 Restful 介面,e.g. `List repos = github.repos();` 64 | 65 | 處理的介面不再是網路相關的 Request ,反倒是刻意隱含了 Request ,導致網路細部操作看似無法處理,所以提供了錯誤處理註冊,`Client` 配置等接口。 66 | 67 | 2. 常見的網路處理: 68 | 69 | * Cache 70 | * Retry 71 | * Time Out 72 | * Cancel 73 | * Priority 74 | 75 | 使用 OkHttpClient 大多可以處理。(AOSP 4.4 之後其實也內建,不確定有沒有包進 SDK) 76 | 77 | 如果沒有用 RxJava Observable 了話,確實 Retry policy 的部份確實有點殘念,也不是無解,只是要自己辛苦點寫 ErrorHandler 重發。 78 | 79 | 使用 RxJava `retry()` 達到重試次數效果: 80 | 81 | ```java 82 | int retryLimit = 3; 83 | Observable repos = github.repos() 84 | .retry(3); 85 | ``` 86 | 87 | 使用 RxJava `retryWhen()` 達到 backoff 效果: 88 | 89 | ```java 90 | int retryLimit = 3; 91 | float backoff = 1.3f 92 | 93 | Observable repos = github.repos() 94 | .retryWhen(attempt -> attempt.zipWith(Observable.range(1, retryLimit), (n, i) -> i) 95 | .flatMap(i -> Observable.timer(i * backoff, TimeUnit.SECONDS)); 96 | ``` 97 | 98 | Cancel 也是,如果沒有 RxJava 你只能寫 ExecutorService 處理或者操作 OkHttpClient。 99 | 100 | 使用 Rxjava `unsubcribe()` 取消: 101 | 102 | ```java 103 | Subscription reposSubscription = repos.subscribe(); 104 | 105 | reposSubscription.unsubscribe(); // 取消 106 | ``` 107 | 108 | 其中比較困難的是 Priority ,需要處理 OkHttpClient executor 。 109 | 110 | 基本上,「RxJava + Retrofit + OkHttpClient(supports SPDY)」一起用,應該是沒什麼太大的問題。 111 | 112 | ## NotRetrofit 113 | 114 | 由於 retrofit 是執行時期處理 annotations 效能有改善的空間。NotRetrofit 是改用編譯時期處理。如同 google fork square/dagger 專案的理由一樣: google/dagger2。 115 | 116 | https://github.com/yongjhih/NotRetrofit 117 | 118 | ## Retrofit 2 119 | 120 | ```java 121 | interface GitHub { 122 | @GET("/repos/{owner}/{repo}/contributors") 123 | Call> contributors( 124 | @Path("owner") String owner, 125 | @Path("repo") String repo); 126 | } 127 | ``` 128 | 129 | 解決 Retrofit 1 長久以來存在的一些問題。不直接回傳物件,而是透過一個中介 `Call`/`Response` 來包裝,使得可以保留網路資訊 Headers 。 130 | 131 | ### 如何取得分頁? 132 | 133 | ```java 134 | interface GitHub { 135 | // ... 136 | @GET 137 | Call> contributorsPaginate(@Url String url); 138 | } 139 | ``` 140 | 141 | ```java 142 | Call> contributorsCall = github.contributors("square", "retrofit"); 143 | Response> contributorsResponse = contributorsCall.execute(); 144 | String nextLink = contributorsResponse.headers().get("Link"); 145 | Call> contributorsNextCall = github.contributorsPaginate(nextLink); 146 | Response> contributorsNextResponse = contributorsNextCall.execute(); 147 | ``` 148 | 149 | ## Retrofit 1 分析 (1.9.0) 150 | 151 | ```java 152 | private class RestHandler implements InvocationHandler { 153 | // ... 154 | public Object invoke(Object proxy, Method method, final Object[] args) { 155 | // sync 156 | return invokeRequest(requestInterceptor, methodInfo, args); 157 | // Rx 158 | return rxSupport.createRequestObservable({ 159 | (ResponseWrapper) invokeRequest(requestInterceptor, methodInfo, args); 160 | }); 161 | // async 162 | Callback callback = (Callback) args[args.length - 1]; // last argument 163 | httpExecutor.execute({ 164 | (ResponseWrapper) invokeRequest(interceptorTape, methodInfo, args); 165 | }); 166 | } 167 | } 168 | ``` 169 | 170 | ## See Also 171 | 172 | * https://github.com/yongjhih/RxJava-retrofit-github-sample 173 | * https://github.com/yongjhih/NotRetrofit 174 | * https://github.com/orhanobut/wasp 175 | -------------------------------------------------------------------------------- /rx-operator.md: -------------------------------------------------------------------------------- 1 | # 開發 RxJava 新增自己的 Operator 2 | 3 | ## 倒序 Observable : OperatorToReversedList 4 | 5 | Before: 6 | 7 | ```java 8 | Observable.range(1, 10).toList().doOnNext(list -> Collections.reverse(list)) 9 | .subscribe(System.out::println); 10 | // [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] 11 | ``` 12 | 13 | After: 14 | 15 | ```java 16 | Observable.range(1, 10).lift(new OperatorToReversedList()) 17 | .subscribe(System.out::println); 18 | ``` 19 | 20 | OperatorToReversedList.java 21 | 22 | ```java 23 | public final class OperatorToReversedList implements Operator, T> { // 進貨 操作員處理後出貨 > 24 | @Override 25 | public Subscriber call(final Subscriber> o) { 26 | return new Subscriber(o) { // 回傳承辦窗口 27 | 28 | final List list = new ArrayList(); 29 | 30 | @Override 31 | public void onStart() { // 開始運作 32 | request(Long.MAX_VALUE); 33 | } 34 | 35 | @Override 36 | public void onCompleted() { // 上一站結束 37 | try { 38 | Collections.reverse(list); 39 | 40 | o.onNext(list); 41 | o.onCompleted(); 42 | } catch (Throwable e) { 43 | onError(e); 44 | } 45 | } 46 | 47 | @Override 48 | public void onError(Throwable e) { // 上一站出狀況 49 | o.onError(e); 50 | } 51 | 52 | @Override 53 | public void onNext(T value) { // 傳遞給下一站 54 | list.add(value); 55 | } 56 | 57 | }; 58 | } 59 | } 60 | ``` 61 | 62 | https://gist.github.com/yongjhih/20ccfab5007ea6bc9f0d 63 | 64 | 實現一個 Operator (操作員) 主要需要回傳一個 Subscriber (承辦窗口) 讓其他人可以塞資料給你,然後等你處理完吐資料出去。 65 | 66 | ```java 67 | // T 是進來的型別 68 | // R 是出去的型別 69 | public class OperatorFrequency implements Operator { 70 | @Override 71 | public Subscriber call(Subscriber child) { ... } // child 下一站的窗口 72 | ``` 73 | 74 | ```java 75 | // Operator 是一個簡單的介面,只要求回傳承辦口以及告知下一站窗口型別 76 | public interface Operator extends Func1, Subscriber> { ... } 77 | ``` 78 | 79 | ```java 80 | // Subscriber 81 | public abstract class Subscriber implements Observer, Subscription { 82 | public abstract void onStart(); 83 | 84 | // public interface Observer { 85 | public abstract void onCompleted(); 86 | public abstract void onError(Throwable e); 87 | public abstract void onNext(T t); 88 | // } 89 | } 90 | ``` 91 | 92 | 93 | 94 | ## OperatorFrequency 每隔一段時間 95 | 96 | ```java 97 | Observable.range(1, 10).lift(new OperatorFrequency(1, TimeUnit.SECONDS)) 98 | .subscribe(i -> System.out.println(i + ": " + System.currentTimeMillis()).subscribe()); 99 | 100 | // 1: 1428053481338 101 | // 2: 1428053482339 102 | // 3: 1428053483338 103 | // 4: 1428053474339 104 | // 5: 1428053475338 105 | // 6: 1428053476338 106 | // 7: 1428053477338 107 | // 8: 1428053478338 108 | // 9: 1428053479338 109 | // 10: 1428053480338 110 | ``` 111 | 112 | OperatorFrequency.java 113 | 114 | ```java 115 | public class OperatorFrequency implements Operator { 116 | private long interval; 117 | private TimeUnit unit; 118 | 119 | public OperatorFrequency(long interval, TimeUnit unit) { 120 | this.interval = interval; 121 | this.unit = unit; 122 | } 123 | 124 | @Override 125 | public Subscriber call(final Subscriber child) { 126 | return new FrequencySubscriber<>(interval, unit, child); 127 | } 128 | 129 | static class FrequencySubscriber extends Subscriber { 130 | private long interval; 131 | private TimeUnit unit; 132 | private final Subscriber child; 133 | private final Observable tick; 134 | private PublishSubject stop = PublishSubject.create(); 135 | private Subject subject; 136 | private Observable zip; 137 | private Subscription subscription; 138 | private long zipCount = 0; 139 | 140 | public FrequencySubscriber(long interval, TimeUnit unit, final Subscriber child) { 141 | super(); 142 | 143 | this.interval = interval; 144 | this.unit = unit; 145 | this.child = child; 146 | 147 | tick = Observable.interval(interval, unit).map(l -> zipCount).distinct().onBackpressureBuffer(1); 148 | subject = PublishSubject.create(); 149 | zip = Observable.zip(subject.asObservable().onBackpressureBuffer(1024), tick, 150 | (emit, t) -> { 151 | zipCount++; 152 | return emit; 153 | }); 154 | } 155 | 156 | @Override 157 | public void onStart() { 158 | if (subscription == null) { 159 | subscription = zip.subscribe(child); 160 | } 161 | } 162 | 163 | @Override 164 | public void onError(Throwable e) { 165 | try { 166 | child.onError(e); 167 | } finally { 168 | unsubscribe(); 169 | } 170 | } 171 | 172 | @Override 173 | public void onCompleted() { 174 | subject.onCompleted(); 175 | } 176 | 177 | @Override 178 | public void onNext(T t) { 179 | subject.onNext(t); 180 | } 181 | } 182 | } 183 | ``` 184 | 185 | https://gist.github.com/yongjhih/ba24c9b3d025333ede17 186 | 187 | # See Also 188 | 189 | * https://github.com/yongjhih/RxJava-Operators/blob/master/src/main/java/com/github/yongjhih/Main.java 190 | * https://github.com/yongjhih/RxJava-GroupByTest/blob/master/src/main/java/rx/internal/operators/OperatorGroupByGroup.java -------------------------------------------------------------------------------- /rxandroid.md: -------------------------------------------------------------------------------- 1 | # RxAndroid 2 | 3 | https://github.com/ReactiveX/RxAndroid 4 | 5 | Android 相關的元件連動 6 | 7 | ## Activity/Fragment: AppObservable 8 | 9 | ```java 10 | AppObservable.bindActivity() 11 | AppObservable.bindFragment() 12 | ``` 13 | 14 | 主要檢查 `Fragment.isAdded()`, `Activity.isFinishing()` 等狀態,排除一些不適當的執行。 15 | 16 | ## RxLifecycle 17 | 18 | 當 Activity/Fragment 對應的生命週期結束時 `unsubscribe()` 避免 leaks。 19 | 20 | `LifecycleObservable` 哪時候訂閱哪時候取消對照表: 21 | 22 | ```java 23 | CREATE -> DESTROY; 24 | START -> STOP; 25 | RESUME -> PAUSE; 26 | PAUSE -> STOP; 27 | STOP -> DESTROY; 28 | ``` 29 | 30 | 手動 `unsubscribe()`: 31 | 32 | ```java 33 | class SimpleActivity extends Activity { 34 | CompsotionSubscription mResumeSubscriptions = new CompositeSubscription(); 35 | 36 | @Override 37 | protected void onResume() { 38 | super.onResume(); 39 | 40 | bindResume(Observable.just("Hello, world"), s -> textView.setText(s)); 41 | } 42 | 43 | protected void bindResume(Observable obs, Action1 onNext) { 44 | mResumeSubscriptions.add(AppObservable.bindActivity(this, obs).subscribe(onNext)); 45 | } 46 | 47 | @Override 48 | protected void onPause() { 49 | super.onPause(); 50 | 51 | mResumeSubscriptions.unsubscribe(); 52 | } 53 | } 54 | ``` 55 | 56 | `RxActivity`/`RxAppCompatActivity`: 57 | 58 | ```java 59 | class SimpleActivity extends RxActivity { 60 | 61 | @Override 62 | public void onResume() { 63 | Observable.just("Hello, world") 64 | .compose(bindToLifecycle()) 65 | .subscribe(s -> textView.setText(s)); 66 | } 67 | } 68 | ``` 69 | 70 | 沒法更換 Activity/Fragment 繼承時,可參考 [RxAppCompatActivity](https://github.com/trello/RxLifecycle/blob/master/rxlifecycle-components/src/main/java/com/trello/rxlifecycle/components/support/RxAppCompatActivity.java)/[RxFragment](https://github.com/trello/RxLifecycle/blob/master/rxlifecycle-components/src/main/java/com/trello/rxlifecycle/components/RxFragment.java) 71 | 72 | 舉例寫一個 SimpleFragmentLifecycleProvider 類別: 73 | 74 | ```java 75 | public class SimpleFragment extends Fragment { 76 | 77 | SimpleFragmentLifecycleProvider lifecycle = new SimpleFragmentLifecycleProvider(); 78 | 79 | @Override 80 | @CallSuper 81 | public void onResume() { 82 | super.onResume(); 83 | lifecycle.onResume(); 84 | 85 | helloObservable 86 | .compose(lifecycle.bindUntil(FragmentEvent.DESTROY)) 87 | .subscribe(); 88 | 89 | helloObservable2 90 | .compose(lifecycle.bind()) 91 | .subscribe(); 92 | } 93 | 94 | @Override 95 | @CallSuper 96 | public void onPause() { 97 | lifecycle.onPause(); 98 | super.onPause(); 99 | } 100 | 101 | @Override 102 | @CallSuper 103 | public void onDestroy() { 104 | lifecycle.onDestroy(); 105 | super.onDestroy(); 106 | } 107 | 108 | // ... 109 | } 110 | 111 | public class SimpleFragmentLifecycleProvider implements FragmentLifecycleProvider { 112 | private final BehaviorSubject lifecycleSubject = BehaviorSubject.create(); 113 | 114 | @Override 115 | public final Observable lifecycle() { 116 | return lifecycleSubject.asObservable(); 117 | } 118 | 119 | @Override 120 | public final Observable.Transformer bindUntil(FragmentEvent event) { 121 | return RxLifecycle.bindUntilFragmentEvent(lifecycleSubject, event); 122 | } 123 | 124 | @Override 125 | public final Observable.Transformer bind() { 126 | return RxLifecycle.bindFragment(lifecycleSubject); 127 | } 128 | 129 | public void onAttach(android.app.Activity activity) { 130 | lifecycleSubject.onNext(FragmentEvent.ATTACH); 131 | } 132 | 133 | public void onCreate(Bundle savedInstanceState) { 134 | lifecycleSubject.onNext(FragmentEvent.CREATE); 135 | } 136 | 137 | public void onViewCreated(View view, Bundle savedInstanceState) { 138 | lifecycleSubject.onNext(FragmentEvent.CREATE_VIEW); 139 | } 140 | 141 | public void onStart() { 142 | lifecycleSubject.onNext(FragmentEvent.START); 143 | } 144 | 145 | public void onResume() { 146 | lifecycleSubject.onNext(FragmentEvent.RESUME); 147 | } 148 | 149 | public void onPause() { 150 | lifecycleSubject.onNext(FragmentEvent.PAUSE); 151 | } 152 | 153 | public void onStop() { 154 | lifecycleSubject.onNext(FragmentEvent.STOP); 155 | } 156 | 157 | public void onDestroyView() { 158 | lifecycleSubject.onNext(FragmentEvent.DESTROY_VIEW); 159 | } 160 | 161 | public void onDestroy() { 162 | lifecycleSubject.onNext(FragmentEvent.DESTROY); 163 | } 164 | 165 | public void onDetach() { 166 | lifecycleSubject.onNext(FragmentEvent.DETACH); 167 | } 168 | } 169 | ``` 170 | 171 | ## ViewObservable, WidgetObservable 172 | 173 | View 的連動. 當 View 顯示時 `subscribe()` 離開時 `unsubscribe()` 174 | 175 | ```java 176 | ViewObservable.bindView() 177 | ``` 178 | 179 | Event 的連動. 180 | 181 | ```java 182 | ViewObservable.clicks() 183 | ``` 184 | 185 | ## See Also 186 | 187 | * [RxActivity](https://github.com/ReactiveX/RxAndroid/blob/master/rxandroid-framework/src/main/java/rx/android/app/RxActivity.java) 188 | * [RxFragmentActivity](https://github.com/ReactiveX/RxAndroid/blob/master/rxandroid-framework/src/main/java/rx/android/app/support/RxFragmentActivity.java) 189 | * [RxFragment](https://github.com/ReactiveX/RxAndroid/blob/master/rxandroid-framework/src/main/java/rx/android/app/support/RxFragment.java) 190 | * [ogaclejapan/RxBinding](https://github.com/ogaclejapan/RxBinding) 191 | * [JakeWharton/RxBinding](https://github.com/JakeWharton/RxBinding) 192 | * https://github.com/trello/RxLifecycle 193 | * https://github.com/ReactiveX/RxAndroid/issues/172 194 | * 註: *RxAndroid 1.0 後,把大部分的功能拆出去 JakeWharton/RxBinding(views 行為連動相關) 與 trello/RxLifecycle(生命週期連動相關)* 195 | -------------------------------------------------------------------------------- /rxbinding.md: -------------------------------------------------------------------------------- 1 | # RxBinding(JakeWharton/RxBinding) 2 | 3 | 讓每個 Android 的元件都包裝成 Rx 。 4 | 5 | RxAndroid Before: 6 | 7 | ```java 8 | ViewObservable.clicks(textView).subscribe(ev -> {}); 9 | ``` 10 | 11 | RxBinding After: 12 | 13 | ```java 14 | RxTextView.textChanged(textView).subscribe(ev -> {}); 15 | ``` 16 | 17 | ## 範例 1 - 驗證過濾 18 | 19 | ```java 20 | Observable rxUsername = RxTextView.textChanges(tvUsername); 21 | Observable rxPassword = RxTextView.textChanges(tvPassword); 22 | Observable rxFullName = RxTextView.textChanges(tvFullName).mergeWith(Observable.just("")); 23 | Observable.combineLatest( 24 | rxUsername, rxPassword, rxFullName, RegistrationModel::new) 25 | .filter(RegistrationModel::isValid) 26 | .subscribe(result -> enableSubmitButton()); 27 | ``` 28 | 29 | ## 範例 2 - 流量控制 30 | 31 | ```java 32 | RxSearchView.queryTextChanges(searchView) 33 | .filter(charSequence -> !TextUtils.isEmpty(charSequence)) 34 | .throttleLast(100, TimeUnit.MILLISECONDS) 35 | .debounce(200, TimeUnit.MILLISECONDS) 36 | .onBackpressureLatest() 37 | .concatMap(charSequence -> { 38 | return searchRepositories(charSequence); 39 | }) 40 | .observeOn(AndroidSchedulers.mainThread()) 41 | .subscribeOn(Schedulers.io()) 42 | .onErrorResumeNext(throwable -> { 43 | return Observable.empty(); 44 | }) 45 | .subscribe(response -> { 46 | showRepositories(response.getItems()); 47 | }); 48 | ``` 49 | 50 | ## 同名 ogaclejapan/RxBinding 51 | 52 | https://github.com/ogaclejapan/RxBinding 53 | 54 | 主要以 MVVM 雙向連動為主要訴求 55 | 56 | Before: 57 | 58 | ```java 59 | class HogeActivity extends Activity { 60 | 61 | @InjectView(R.id.text) 62 | private TextView mTextView; 63 | 64 | @Override 65 | protected void onCreate(Bundle savedInstanceState) { 66 | super.onCreate(savedInstanceState); 67 | setContentView(R.layout.activity_main); 68 | ButterKnife.inject(this); 69 | 70 | AppObservable.bindActivity(this, Observable.just("hoge")) 71 | .subscribeOn(Schedulers.io()) 72 | .subscribe(setTextAction()); 73 | } 74 | 75 | Action1 setTextAction() { 76 | return text -> mTextView.setText(text); 77 | } 78 | } 79 | ``` 80 | 81 | After: 82 | 83 | ```java 84 | class HogeActivity extends Activity { 85 | // ... 86 | 87 | private Rx mRxTextView 88 | 89 | @Override 90 | protected void onCreate(Bundle savedInstanceState) { 91 | super.onCreate(savedInstanceState); 92 | setContentView(R.layout.activity_main); 93 | ButterKnife.inject(this); 94 | 95 | mRxTextView = RxView.of(mTextView); 96 | Subscription s = mRxTextView.bind(Observable.just("hoge"), RxActions.setText()); 97 | } 98 | } 99 | ``` 100 | 101 | ## See Also 102 | 103 | * Jack Wharton 的 RxJava 電台訪談: http://fragmentedpodcast.com/episodes/7/ 104 | * https://github.com/JakeWharton/RxBinding 105 | * https://github.com/ogaclejapan/RxBinding 106 | * https://medium.com/@diolor/improving-ux-with-rxjava-4440a13b157f 107 | -------------------------------------------------------------------------------- /setup-wizard.md: -------------------------------------------------------------------------------- 1 | # 安裝精靈 2 | 3 | 最近有人問到,setup wizard 安裝精靈沒有被呼叫起來的問題。主要四年前有配置出廠預設過,device/{product}/ , 4 | 印象比較深刻的是 device_provisioned ,這個在 Android 系統隱私資料庫內(Settings/DatabaseHelper) 也有記載,主要得知是否已經配置過 provisioned 。 5 | 6 | 一種是正向追 code : 7 | 8 | ```sh 9 | sgrep -i device_provisioned 10 | ``` 11 | 12 | 另一種,逆向追 code 是誰負責呼叫你的?安插在系統的 broadcaster 身上,印出 setup wizard caller 是誰。這個比較黑手一點,當作最終手段吧。 13 | 14 | 再來另一個線索是,setup wizard 的 manifest ,因為系統還是依據 manifest 某個特殊條件才會呼叫的: 15 | 16 | 線索一: 17 | 18 | ```xml 19 | 20 | ``` 21 | 22 | 線索二: 23 | 24 | ```xml 25 | 26 | ``` 27 | 28 | 所以我們還可以 `sgrep -i SETUP_VERSION` 以及 `sgrep -i DEVICE_INITIALIZATION_WIZARD`,最終,我們可以找到 ActivityManagerService.java 29 | 30 | ```java 31 | void startSetupActivityLocked() { 32 | // ... 33 | 34 | // We will show this screen if the current one is a different 35 | // version than the last one shown, and we are not running in 36 | // low-level factory test mode. 37 | final ContentResolver resolver = mContext.getContentResolver(); 38 | if (mFactoryTest != SystemServer.FACTORY_TEST_LOW_LEVEL && 39 | Settings.Secure.getInt(resolver, 40 | Settings.Secure.DEVICE_PROVISIONED, 0) != 0) { 41 | 42 | // ... 43 | 44 | ResolveInfo ri = null; 45 | for (int i=0; ris != null && i e.length() > 3) 12 | .peek(e -> System.out.println("Filtered value: " + e)) 13 | .map(String::toUpperCase) 14 | .peek(e -> System.out.println("Mapped value: " + e)) 15 | .collect(Collectors.toList()); 16 | ``` 17 | 18 | After: 19 | 20 | ```java 21 | public static List getNames(List users) { 22 | return StreamSupport.stream(users).map(user -> user.name()).collect(Collectors.toList()); 23 | } 24 | ``` 25 | 26 | After: 27 | 28 | ```java 29 | public static String[] getNames(User[] users) { 30 | return J8Arrays.stream(users).map(user -> user.name()).toArray(length -> new String[length]); 31 | } 32 | ``` 33 | 34 | ## 其他方案 35 | 36 | * https://github.com/kgmyshin/Marray 37 | * https://github.com/konmik/solid 38 | * https://github.com/goldmansachs/gs-collections 39 | * Agera 40 | 41 | 特點: 42 | 43 | * Marray 可修改 44 | * SolidList 主要以 ImmutableList 出發,所以沒實現修改能力 45 | * SolidList 支援 Parcelable 傳遞 46 | * 知名 gs-collections 不過似乎不夠輕 47 | 48 | 另外還有其他 Promise 選用: 49 | 50 | * [Bolt-Android](bolts-android.md) 51 | 52 | ## Collections 資料流 53 | 54 | ### map 55 | 56 | Java 8: 57 | 58 | ```java 59 | List list = Arrays.asList(1, 2, 3).stream().map(String::valueOf).collect(Collectors.toList()); 60 | ``` 61 | 62 | javaslang: 63 | 64 | ```java 65 | List list = Stream.of(1, 2, 3).map(String::valueOf).toJavaList(); 66 | // or 67 | List list = javaslang.collection.List.of(1, 2, 3).map(String::valueOf).toJavaList(); 68 | ``` 69 | 70 | GS-Collections: 71 | 72 | ```java 73 | List list = Lists.mutable.of(1, 2, 3).collect(String::valueOf).toList(); 74 | // or lazy 75 | List lazyList = Lists.mutable.of(1, 2, 3).asLazy().collect(String::valueOf).toList(); 76 | ``` 77 | 78 | RxJava: 79 | 80 | ```java 81 | List list = Observable.from(Arrays.asList(1, 2, 3)).map(String::valueOf).toList().toBlocking().single(); 82 | ``` 83 | 84 | ### count 85 | 86 | Java 8: 87 | 88 | ```java 89 | long evens = Arrays.asList(1, 2, 3).stream().filter(each -> each % 2 == 0).count(); 90 | ``` 91 | 92 | javaslang: 93 | 94 | ```java 95 | long evens = javaslang.collection.List.of(1, 2, 3).filter(each -> each % 2 == 0).count(); 96 | ``` 97 | 98 | GS-Collections: 99 | 100 | ```java 101 | int evens = Lists.mutable.of(1, 2, 3).count(each -> each % 2 == 0); 102 | ``` 103 | 104 | RxJava: 105 | 106 | ```java 107 | int evens = Observable.from(Arrays.asList(1, 2, 3)).filter(each -> each % 2 == 0).count().toBlocking().single(); 108 | ``` 109 | 110 | * io.javaslang:javaslang:2.0.2 111 | * com.goldmansachs:gs-collections:7.0.0 112 | 113 | ## See Also 114 | 115 | * https://sourceforge.net/projects/streamsupport/ 116 | * https://github.com/yongjhih/streamsupport 117 | * http://verhoevenv.github.io/2015/08/18/fluentiterable-streamsupport-java8.html 118 | * https://github.com/goldmansachs/gs-collections 119 | * http://www.goldmansachs.com/gs-collections/documents/GS%20Collections%20Training%20Session%20and%20Kata%205.0.0.pdf 120 | * https://github.com/javaslang/javaslang 121 | 122 | -------------------------------------------------------------------------------- /styles/website.css: -------------------------------------------------------------------------------- 1 | /* CSS for website */ 2 | 3 | .book-body p , .book-body h1,.book-body h2,.book-body h3,.book-body h4,.book-body h5,.book-body h6,.book-body div,.book-body span , .summary , .summary li , .summary li a { 4 | font-family : medium-content-sans-serif-font,"Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Verdana, "Microsoft Yahei UI", "Microsoft Yahei",sans-serif; 5 | } 6 | 7 | 8 | .book .book-body .page-wrapper .page-inner section.normal code, 9 | .book .book-body .page-wrapper .page-inner section.normal code * { 10 | font-family: Menlo, Monaco, Consolas, Courier, "WenQuanYi Micro Hei Mono", "Courier New", monospace; 11 | font-size: 13px !important; 12 | } 13 | 14 | /* yahei */ 15 | 16 | /* 17 | .book-body p , .book-body h1,.book-body h2,.book-body h3,.book-body h4,.book-body h5,.book-body h6,.book-body div,.book-body span , .summary , .summary li , .summary li a { 18 | font-family : "Open Sans", "Clear Sans", "Helvetica Neue", Helvetica, Arial, "Microsoft Yahei UI", "Microsoft Yahei", sans-serif; 19 | } 20 | 21 | 22 | .book .book-body .page-wrapper .page-inner section.normal code { 23 | font-family: Consolas, Menlo, Courier, 'WenQuanYi Micro Hei Mono', monospace; 24 | } 25 | */ -------------------------------------------------------------------------------- /test_-_espresso,_rxpresso.md: -------------------------------------------------------------------------------- 1 | # Test - UI 2 | 3 | UI 測試框架: 4 | 5 | * Robotium 6 | * uiautomator 7 | * espresso 8 | * Appium 9 | * Calabash 10 | 11 | 跨 iOS 使用 Appium 或 Calabash 12 | 13 | ## RxPresso 14 | 15 | ```java 16 | rxPresso.given(mockedRepo.getUser("id")) 17 | .withEventsFrom(Observable.just(new User("some name"))) 18 | .expect(any(User.class)) 19 | .thenOnView(withText("some name")) 20 | .perform(click()); 21 | ``` 22 | 23 | ## See Also 24 | 25 | * https://github.com/novoda/rxpresso 26 | -------------------------------------------------------------------------------- /test_-_robolectric.md: -------------------------------------------------------------------------------- 1 | # Test - robolectric 2 | 3 | ![robolectric](http://robolectric.org/images/robolectric-stacked-3f7ad42c.png) 4 | 5 | robolectric 提供 Android 相關的實體,例如:`Activity`, `Context` 以便離線測試。 6 | 7 | ```java 8 | @RunWith(RobolectricTestRunner.class) 9 | public class MyActivityTest { 10 | 11 | @Test 12 | public void clickingButton_shouldChangeResultsViewText() throws Exception { 13 | MyActivity activity = Robolectric.setupActivity(MyActivity.class); 14 | 15 | Button button = (Button) activity.findViewById(R.id.button); 16 | TextView results = (TextView) activity.findViewById(R.id.results); 17 | 18 | button.performClick(); 19 | assertThat(results.getText().toString()).isEqualTo("Robolectric Rocks!"); 20 | } 21 | } 22 | ``` -------------------------------------------------------------------------------- /theme.md: -------------------------------------------------------------------------------- 1 | # 佈景 Theme 2 | 3 | 常用的佈景架構: 4 | 5 | res/values/theme.xml: 6 | 7 | ```xml 8 | 9 | 10 | 11 | @color/md_teal_400 12 | @color/md_teal_500 13 | 14 | 15 | 19 | 20 | 21 | 23 | 24 | 31 | 32 | 37 | 38 | 42 | 43 | 44 | 49 | 50 | 51 | 55 | 56 | 57 | 64 | 65 | 68 | 69 | 73 | 74 | ``` 75 | -------------------------------------------------------------------------------- /zh-cn/README.md: -------------------------------------------------------------------------------- 1 | # Android App 开源开发 2 | 3 | * gitbook: http://yongjhih.gitbooks.io/feed/ 4 | 5 | [![](cover.jpg)](http://yongjhih.gitbooks.io/feed/) 6 | 7 | 12 | 13 | * github: https://github.com/yongjhih/android-gitbook/ 14 | 15 | 开放书籍,欢饮共写修改,以 gitbook 方式浏览时,可点击页面上方的编辑页面链接 "EDIT THIS PAGE" 即可共写。 16 | 17 | 善用开源来开发 Android App。 18 | 19 | 前端: 20 | 21 | * RxJava, RxAndroid 22 | * retrolambda (java7 + java8) 23 | * retrofit, [NotRetrofit](https://github.com/yongjhih/NotRetrofit) 24 | * okhttp 25 | * DI: Dagger2 26 | * AutoParcel, AutoValue, etc. 27 | * ImageLoader: fresco, AUIL, picasso, glide, etc. 28 | * json2pojo: jackson, gson, logansquare, etc. 29 | * Orm: DBFlow, etc. 30 | * SimpleFacebook, [RetroFacebook](https://github.com/yongjhih/RetroFacebook) 31 | * EventBus/Otto 32 | * mockito 33 | * espresso 34 | * robolectric 35 | * assertj(fest), truth 36 | * icepick 37 | * [auto-parse](https://github.com/yongjhih/auto-parse) 38 | * [RxParse](https://github.com/yongjhih/RxParse) 39 | * [RxFacebook](https://github.com/yongjhih/RxFacebook) 40 | * [proguard annotations](https://github.com/yongjhih/proguard-annotations) 41 | * [proguard snippets](https://github.com/yongjhih/proguard-snippets) 42 | * [json2notification](https://github.com/8tory/json2notification) 43 | * Kotlin 44 | * anko 45 | * ReactNative 46 | * jitpack 47 | * bintray 48 | * bountysource 49 | * gitter 50 | * travis-ci 51 | 52 | 后端: 53 | 54 | * Parse BAAS 55 | 56 | 开发环境: 57 | 58 | * Android Studio 59 | * gradle 60 | 61 | 编辑环境: 62 | 63 | * gitlab 64 | * phabricator 65 | 66 | 持续整合环境: 67 | 68 | * jenkins 69 | 70 | ## 版权 71 | 72 | [![CC-BY 4.0](http://creativecommons.tw/sites/creativecommons.tw/files/cc-by.png)](https://creativecommons.org/licenses/by/4.0/legalcode.txt) 73 | -------------------------------------------------------------------------------- /zh-cn/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Introduction](README.md) 4 | * [RxJava](RxJava.md) 5 | * [RxAndroid](rxandroid.md) 6 | * [RxBinding](notrxandroid.md) 7 | * [开发 RxJava 新增自己的 Operator](rx_operator.md) 8 | * [资料流替代方案](data_stream.md) 9 | * [Lambda](lambda.md) 10 | * [Bolts-Android](bolts-android.md) 11 | * [AutoValue](autovalue.md) 12 | * [Dagger2](dagger2.md) 13 | * [Annotation Programming](annotation_programming.md) 14 | * [RecyclerView, TwoWayView](recyclerview_twowayview.md) 15 | * [Android Support Annotations](android_support_annotations.md) 16 | * [Image Loader - Android-Universal-Image-Loader, picasso, glide, fresco](image_loader_android-universal-image-loader,_picasso,_glide,_fresco.md) 17 | * [json to POJO](json_to_pojo.md) 18 | * [Clean architecture](clean_architecture.md) 19 | * [Retrofit](retrofit.md) 20 | * [Orm - ActiveAndroid, DBFlow, Ollie](orm_-_activeandroid,_dbflow,_ollie.md) 21 | * [flow + mortar](flow_mortar.md) 22 | * [EventBus: greenrobot/EventBus, Square/otto](eventbus__otto.md) 23 | * 良好的撰写习惯 JavaDoc 24 | * [开源套件库设置 - jcenter(), mavenCentral()](openpackage_setup_jcenter_,_mavencentral.md) 25 | * 开源码函式库专案设置 - Github 26 | * [Test - Assert 断言 - assertj, truth](assert.md) 27 | * [Test - Mockito](mockito.md) 28 | * [Test - UI - espresso, rxpresso](test_-_espresso,_rxpresso.md) 29 | * [Debug 除错 - stetho, leakcanary](debug_stetho_,_leakcanary.md) 30 | * [Test - robolectric](test_-_robolectric.md) 31 | * [Kotlin](kotlin.md) 32 | * [Aspect](aspect.md) 33 | * [AccountManager](accountmanager.md) 34 | * [Notification](notification.md) 35 | * [开源八卦](kai_yuan_ba_gua.md) 36 | * [经典问题 - 指引你「这该怎么弄?」](FAQ.md) 37 | * [作者群介绍](authors.md) 38 | * [docker](docker.md) 39 | * [资源](resources.md) 40 | * [Parse](parse.md) 41 | * [gradle](gradle.md) 42 | * [React Native](react_native.md) 43 | * [Android SDK](android_sdk.md) 44 | * [adb](adb.md) 45 | * [Theme](theme.md) 46 | 47 | -------------------------------------------------------------------------------- /zh-cn/rxandroid.md: -------------------------------------------------------------------------------- 1 | # RxAndroid 2 | 3 | https://github.com/ReactiveX/RxAndroid 4 | 5 | 生命周期的连动。 6 | 7 | ## AppObservable 8 | 9 | ```java 10 | AppObservable.bindActivity() 11 | AppObservable.bindFragment() 12 | ``` 13 | 14 | 主要检查 `Fragment.isAdded()`, `Activity.isFinishing()`。 15 | 16 | *注:笔者不是很清楚,为什么不用 overloading: `AppObservable.bind(Activity/Frgment/v4.Fragment)` 来取代 `AppObservable.bindFragment(Fragment)`, 17 | `AppObservable.bindFragment(v4.Fragment)`, 18 | `AppObservable.bindActivity(Activity)`* 19 | 20 | ## LifecycleObservable 21 | 22 | ```java 23 | LifecycleObservable.bindActivityLifecycle() 24 | LifecycleObservable.bindFragmentLifecycle() 25 | ``` 26 | 27 | 当 Activty/Fragment 对应的生命周期结束时,自动 `unsubscribe()`。 28 | 29 | `LifecycleObservable` “什么时候订阅什么时候取消”对照表 30 | 31 | ```java 32 | CREATE -> LifecycleEvent.DESTROY; 33 | START -> LifecycleEvent.STOP; 34 | RESUME -> LifecycleEvent.PAUSE; 35 | PAUSE -> LifecycleEvent.STOP; 36 | STOP -> LifecycleEvent.DESTROY; 37 | ``` 38 | 39 | 手动自己 `unsubscribe()`, 如果 Activity 要結束,把一些 subscriptions 取消: 40 | 41 | ```java 42 | class SimpleActivity extends Activity { 43 | CompsotionSubscription mSubscriptions = new CompositeSubscription(); 44 | 45 | @Override 46 | protected void onResume() { 47 | super.onResume(); 48 | 49 | bind(Observable.just("Hello, world"), s -> textView.setText(s)); 50 | } 51 | 52 | protected void bind(Observable obs, Action1 onNext) { 53 | mCompositeSubscription.add(AppObservable.bindActivity(this, obs).subscribe(onNext)); 54 | } 55 | 56 | @Override 57 | protected void onDestroy() { 58 | super.onDestroy(); 59 | 60 | mCompositeSubscription.unsubscribe(); 61 | } 62 | } 63 | ``` 64 | 65 | `LifecycleObservable` + `RxActivity`: 66 | 67 | ```java 68 | class SimpleActivity extends RxActivity { 69 | 70 | @Override 71 | public void onResume() { 72 | LifecycleObservable.bindActivityLifecycle(lifecycle(), 73 | AppObservable.bindActivity(this, Observable.just("Hello, world"))) 74 | ).subscribe(s -> textView.setText(s)); 75 | } 76 | } 77 | ``` 78 | 79 | ## ViewObservable, WidgetObservable 80 | 81 | View 的连动. 当 View 显示时 `subscribe()` 离开时 `unsubscribe()` 82 | 83 | ```java 84 | ViewObservable.bindView() 85 | ``` 86 | 87 | Event 的连动. 88 | 89 | ```java 90 | ViewObservable.clicks() 91 | ``` 92 | 93 | ## ogaclejapan/RxBinding 94 | 95 | https://github.com/ogaclejapan/RxBinding 96 | 97 | 类似于 ViewObservable。主要以 MVVM 双向连动作努力。 98 | 99 | Before: 100 | 101 | ```java 102 | class HogeActivity extends Activity { 103 | 104 | @InjectView(R.id.text) 105 | private TextView mTextView; 106 | 107 | @Override 108 | protected void onCreate(Bundle savedInstanceState) { 109 | super.onCreate(savedInstanceState); 110 | setContentView(R.layout.activity_main); 111 | ButterKnife.inject(this); 112 | 113 | AppObservable.bindActivity(this, Observable.just("hoge")) 114 | .subscribeOn(Schedulers.io()) 115 | .subscribe(setTextAction()); 116 | } 117 | 118 | Action1 setTextAction() { 119 | return text -> mTextView.setText(text); 120 | } 121 | } 122 | ``` 123 | 124 | After: 125 | 126 | ```java 127 | class HogeActivity extends Activity { 128 | // ... 129 | 130 | private Rx mRxTextView 131 | 132 | @Override 133 | protected void onCreate(Bundle savedInstanceState) { 134 | super.onCreate(savedInstanceState); 135 | setContentView(R.layout.activity_main); 136 | ButterKnife.inject(this); 137 | 138 | mRxTextView = RxView.of(mTextView); 139 | Subscription s = mRxTextView.bind(Observable.just("hoge"), RxActions.setText()); 140 | } 141 | } 142 | ``` 143 | 144 | ## RxLifecycle 145 | 146 | ## See Also 147 | 148 | * [RxActivity](https://github.com/ReactiveX/RxAndroid/blob/master/rxandroid-framework/src/main/java/rx/android/app/RxActivity.java) 149 | * [RxFragmentActivity](https://github.com/ReactiveX/RxAndroid/blob/master/rxandroid-framework/src/main/java/rx/android/app/support/RxFragmentActivity.java) 150 | * [RxFragment](https://github.com/ReactiveX/RxAndroid/blob/master/rxandroid-framework/src/main/java/rx/android/app/support/RxFragment.java) 151 | * [ogaclejapan/RxBinding](https://github.com/ogaclejapan/RxBinding) 152 | * [JakeWharton/RxBinding](https://github.com/JakeWharton/RxBinding) 153 | * https://github.com/trello/RxLifecycle 154 | * https://github.com/ReactiveX/RxAndroid/issues/172 155 | 156 | *注:并没有 RxAppCompatActivity* 157 | --------------------------------------------------------------------------------