├── .DS_Store ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── android-blog └── readme.md ├── androidweekly ├── .DS_Store ├── Android REST客户端库Retrofit的小教程 │ └── README.md ├── Android测试框架:Dagger 2 + Espresso 2 + Mockito │ └── README.md ├── ListView或者RecycleView滚动时隐藏Toolbar(1) │ ├── images │ │ ├── broken_gif.gif │ │ ├── clipped.png │ │ ├── clipping_fixed.png │ │ ├── demo_gif (1).gif │ │ └── demo_gif.gif │ └── readme.md ├── ListView或者RecycleView滚动时隐藏Toolbar(2) │ ├── images │ │ ├── goal.gif │ │ ├── nosnap.gif │ │ └── playstore.gif │ └── readme.md ├── readme.md ├── 一个支持多设备的Android参考应用 │ └── readme.md ├── 一种在android中实现MVP模式的新思路 │ └── Android MVP - An Alternate Approach.md ├── 欢迎来到Android多进程时代 │ ├── .DS_Store │ ├── images │ │ ├── img01.gif │ │ ├── img02.png │ │ ├── img03.png │ │ └── img04.gif │ └── readme.md ├── 让你的Android应用能使用多种主题-Part-1 │ ├── .DS_Store │ ├── images │ │ ├── multiple-theme-dark.png │ │ └── multiple-theme-light.png │ └── readme.md ├── 让你的Android应用能使用多种主题-Part-2 + chaossss │ └── template.md └── 那些年我们错过的响应式编程 │ ├── images │ ├── 3324-amazed-face.gif │ ├── zclickstream.png │ ├── zmantra.jpg │ ├── zmulticlickstream.png │ ├── zresponsemetastream.png │ ├── zresponsestream.png │ └── ztwitterbox.png │ └── readme.md ├── others ├── .DS_Store ├── readme.md └── 一种更清晰的Android架构 │ └── readme.md └── template.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android开发技术前线 ( android-tech-frontier ) 2 | 一个定期翻译、发布国内外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目,让我们的技术跟上国际步伐。 3 | 4 | 5 | ## 目录 6 | * [文章分类](#category) 7 | * [优秀推荐文章](#recommend) 8 | * [已完成文章列表](#articles) 9 | 10 | ## 参与步骤 11 | 1. 将该项目fork到自己的github; 12 | 2. 在对应分类里面创建一个文件夹,文件夹命名格式为: 中文标题名,比如Android MVP模式与实践; 13 | 3. 在该文件夹中创建readme.md和images文件夹(存放文章图片,宽度尽量控制在400 px左右); 14 | 3. 将模板[template.md](template.md)中的内容拷贝到readme.md,按照模板填写、翻译完内容,完成翻译后将状态修改为"校对中",提交pull request到本项目; 15 | 4. 管理员校对完成之后便会发布. 16 | 17 | **翻译群: 399424408, Android框架设计交流群 : 413864859.** 18 | 19 | ## 微信订阅号 ( 最新文章,及时推送,赶紧扫描关注吧! ) 20 | ![weixin](http://img.blog.csdn.net/20150320083829337) 21 | 22 | 23 | ## 文章分类 24 | | 来源 | 介绍 | 25 | |----------|-------------| 26 | | [AndroidWeekly](androidweekly) | 每周更新国外的技术咨询、开源库等信息[AndroidWeekly](http://androidweekly.net/) | 27 | | [Android Blog](android-blog) | Android官方技术博客[Android Dev Blog](http://android-developers.blogspot.com/) | 28 | | [Others](others) | 其他来源的优秀文章 | 29 | 30 | 31 | ## 推荐文章 32 | 如果您有好的文章推荐我们翻译,请在 在[bboyfeiyu/android-tech-frontier](https://github.com/bboyfeiyu/android-tech-frontier/issues) 下提一个issue,Issue 模板: 33 | https://github.com/bboyfeiyu/android-tech-frontier/issues/1 34 | 35 | * 推荐理由: `这是一篇关于XXXXXXX的优秀文章` 36 | * 原文链接: 写明原始链接 37 | * 标签: `推荐`, 来源,如`androidweekly`,`android-blog`等 38 | * milestone: 设置为当月,如`2015/03` 39 | 40 | 41 | ## 翻译任务跟踪和管理 42 | 43 | 有想要翻译文章的朋友情从issue列表中选择一个翻译任务,然后在issue中添加一个评论,将自己的github账户添加到评论中( 比如 : @Mr.Simple认领该翻译任务 ),管理员会将该issue的标签设置为翻译中。翻译者及时更新翻译状态,状态通过标签跟踪: 44 | 45 | * 待认领 46 | * 翻译中 47 | * 翻译完成 48 | * 校对中 49 | * 校对完成 50 | * 已发布 51 | 52 | 译者翻译完成之后想本项目发布pull request,校对完成之后会合并您的提交,并且将对应的issue关闭。 53 | 54 | 55 | 56 | ## 已完成列表 57 | ### 2015.3.27 ( 第二期) 58 | 59 | 60 | 61 | ### 2015.3.22 ( 第一期) 62 | | 文章名称 | 译者 | 出处 | 63 | |---------|--------|-------| 64 | | [一种在android中实现MVP模式的新思路](androidweekly/一种在android中实现MVP模式的新思路) | [FTExplore](https://github.com/FTExplore) | [AndroidWeekly issue #144](http://blog.cainwong.com/android-mvp-an-alternate-approach/) | 65 | | [一种更清晰的Android架构](others/一种更清晰的Android架构) | [Mr.Simple](https://github.com/bboyfeiyu) | [http://fernandocejas.com](http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/) | 66 | | [一个支持多设备的Android参考应用](androidweekly/一个支持多设备的Android参考应用) | [Mr.Simple](https://github.com/bboyfeiyu) | [AndroidWeekly issue #144](http://androidweekly.net/issues/issue-144) | 67 | | [让你的Android应用能使用多种主题 (Part 1)](androidweekly/让你的Android应用能使用多种主题-Part-1) | [chaossss](https://github.com/chaossss) | [AndroidWeekly issue #144](http://androidweekly.net/issues/issue-144) | 68 | | [欢迎来到Android多进程时代](androidweekly/欢迎来到Android多进程时代) | [Lollypo](https://github.com/Lollypo) | [AndroidWeekly issue #144](http://androidweekly.net/issues/issue-144) | 69 | -------------------------------------------------------------------------------- /android-blog/readme.md: -------------------------------------------------------------------------------- 1 | ## Android官方博客 -------------------------------------------------------------------------------- /androidweekly/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/.DS_Store -------------------------------------------------------------------------------- /androidweekly/Android REST客户端库Retrofit的小教程/README.md: -------------------------------------------------------------------------------- 1 | ## Android REST客户端库Retrofit的小教程 2 | (by [@makeinfo14](http://themakeinfo.com/author/makeinfo14/)) 3 | --- 4 | 5 | > 6 | * 原文链接 : [Retrofit Android Tutorial](http://themakeinfo.com/2015/04/retrofit-android-tutorial/) 7 | * 译者 : [yaoqinwei](https://github.com/yaoqinwei) 8 | * 校对者: [](github链接) 9 | * 状态 : 待校对 10 | 11 | 12 | 13 | 14 | Little tutorial how to use Retrofit to write a Android client for your REST API. 15 | 16 | 这是一篇关于如何使用Retrofit写一个Android的REST客户端的小教程。 17 | 18 | ![Retrofit+Android](http://img.my.csdn.net/uploads/201504/13/1428932650_8819.jpg) 19 | 20 | ## Why I chose Retrofit ? 21 | 22 | Before using [Retrofit](http://square.github.io/retrofit/) from square, I tried volley (from Google) and AsyncTask. After using retrofit,my works gets more easy.You must read the below topics before jumping into this Tutorial.This is a beginner based project which gives an idea of retrieving from api using Retrofit. 23 | 24 | This project is also added to [my Github](https://github.com/basil2style/Retrofit-Android-Basic) 25 | 26 | [Comparison of AsyncHttp ,Volley and Retrofit](https://instructure.github.io/blog/2013/12/09/volley-vs-retrofit/) 27 | 28 | ![volley-vs-retrofit](http://img.my.csdn.net/uploads/201504/13/1428929622_5444.png) 29 | 30 | Volley is a small library compared to Retrofit ,but it is undocumented. Retrofit is developed by [Square](https://github.com/square),also famous for okhttp,picasso..etc(you can find it from [here](https://square.github.io/#android)).If you need volley ,then you can from [Google Training](https://developer.android.com/training/volley/index.html) or [Volley Plus from DWork](https://github.com/DWorkS/VolleyPlus). 31 | 32 | ## 我为什么选择Retrofit? 33 | 34 | 在使用square的[Retrofit](http://square.github.io/retrofit/)之前,我尝试过Volley和AsyncTask。但在使用过Retrofit之后,我的工作变得更加简单了。在开始阅读教程之前,建议先阅读一下下面的几个话题。这是一个入门项目,可以让你了解如何使用Retrofit从API获取数据。 35 | 36 | 这个项目也加到了[我的Github](https://github.com/basil2style/Retrofit-Android-Basic)中。 37 | 38 | [AsyncHttp ,Volley和Retrofit的对比](https://instructure.github.io/blog/2013/12/09/volley-vs-retrofit/) 39 | 40 | ![volley-vs-retrofit](http://img.my.csdn.net/uploads/201504/13/1428929622_5444.png) 41 | 42 | 与Retrofit相比,Volley是一个小型库,而且缺乏官方文档。Retrofit是[Square](https://github.com/square)开发的,后者还开发过okhttp,picasso...等一些著名的库(你可以在[这里](https://square.github.io/#android)找到其他的库)。如果你需要Volley的指引,你可以在[Google Training](https://developer.android.com/training/volley/index.html)或者[Volley Plus from DWork](https://github.com/DWorkS/VolleyPlus)找到相关文档。 43 | 44 | ## Introduction 45 | 46 | [Retrofit](http://square.github.io/retrofit/) is a REST Client for Android and Java by [Square](http://square.github.io/).This library is easy learn and has more features.This is beginner friendly compared to other Networking libraries.You can GET,POST,PUT,DELETE ..etc using this library. You can also use picasso for image loading.[Read this](https://www.bignerdranch.com/blog/solving-the-android-image-loading-problem-volley-vs-picasso/) before using Picasso or Volley. 47 | 48 | Get rid of the Introduction and Lets star the Coding !!! 49 | 50 | We are using Github Api for this App : https://api.github.com/users/basil2style 51 | 52 | You can search github users details using this demo app :) 53 | 54 | [GITHUB](https://github.com/basil2style/Retrofit-Android-Basic) 55 | 56 | ![Download-Code](http://img.my.csdn.net/uploads/201504/13/1428929608_5673.png) 57 | 58 | [Download APK ](https://github.com/basil2style/Retrofit-Android-Basic/blob/master/APK/Retrofit%20Example.apk) 59 | 60 | qr code 61 | 62 | ## 简介 63 | 64 | [Retrofit](http://square.github.io/retrofit/)是[Square](http://square.github.io/)开发的一个Android和Java的REST客户端库。这个库非常简单并且具有很多特性,相比其他的网络库,更容易让初学者快速掌握。它可以处理GET、POST、PUT、DELETE...等请求,还可以使用picasso加载图片。在使用Picasso或Volley之前,可以先来读读[这个](https://www.bignerdranch.com/blog/solving-the-android-image-loading-problem-volley-vs-picasso/)。 65 | 66 | 别纠结简介了,开始编码吧!!! 67 | 68 | Demo里使用是Github的API : https://api.github.com/users/basil2style 69 | 70 | 你可以使用这个Demo App来搜索github的用户详细信息 71 | 72 | [GITHUB](https://github.com/basil2style/Retrofit-Android-Basic) 73 | 74 | ![Download-Code](http://img.my.csdn.net/uploads/201504/13/1428929608_5673.png) 75 | 76 | [Download APK ](https://github.com/basil2style/Retrofit-Android-Basic/blob/master/APK/Retrofit%20Example.apk) 77 | 78 | qr code 79 | 80 | ## 1) Overview 81 | 82 | ![Retrofit-3-classes](http://img.my.csdn.net/uploads/201504/13/1428929609_1240.png) 83 | 84 | In Retrofit,we need to create 3 classes. 85 | 86 | 1) **POJO (Plain Old Java Object) or Model Class** : The json retrieved from the server is added to this class. 87 | 88 | 2) **Interface** : Now we need to create an interface for managing url calls like GET,POST..etc.This is the service class. 89 | 90 | 3) **RestAdapter Class** : This is RestClient Class. Gson is used in default for the retrofit.You can use setup your own converter for this purpose like jackson which will describe on later tutorials. 91 | 92 | ## 1) 概述 93 | 94 | ![Retrofit-3-classes](http://img.my.csdn.net/uploads/201504/13/1428929609_1240.png) 95 | 96 | 1) **POJO或模型实体类** : 从服务器获取的JSON数据将被填充到这种类的实例中。 97 | 98 | 2) **接口** : 我们需要创建一个接口来管理像GET,POST...等请求的URL,这是一个服务类。 99 | 100 | 3) **RestAdapter类** : 这是一个REST客户端(RestClient)类,retrofit中默认用的是Gson来解析JSON数据,你也可以设置自己的JSON解析器,比如jackson,我们将在下面的教程中详细解说明。 101 | 102 | ## 2) Adding Retrofit Library to Project 103 | 104 | _**For Gradle**_ : 105 | 106 | ```groovy 107 | compile 'com.squareup.retrofit:retrofit:1.9.0' 108 | ``` 109 | Currently,1.9.0 is the latest version.You can get updated version from here 110 | 111 | _**JAR**_ : 112 | 113 | [Download jar](https://search.maven.org/remote_content?g=com.squareup.retrofit&a=retrofit&v=LATEST) 114 | 115 | ## 2) 添加Retrofit库到项目中 116 | 117 | _**Gradle**_ : 118 | 119 | ```groovy 120 | compile 'com.squareup.retrofit:retrofit:1.9.0' 121 | ``` 122 | 目前,1.9.0是最新的版本. 你可以在[这里](https://github.com/square/retrofit)获取更新的版本。 123 | 124 | _**JAR**_ : 125 | 126 | [下载Jar包](https://search.maven.org/remote_content?g=com.squareup.retrofit&a=retrofit&v=LATEST) 127 | 128 | ## 3) Create Android Project 129 | 130 | 1) Creating New Project in Android Studio is by : **File => New Project** and fill the descriptions and click **Next**. 131 | 132 | 2) Fill the **minimum SDK** for the project, i use **4.0+** (Retrofit requires Android **2.3+** or Java **6**) 133 | 134 | 3) Select **Blank Activity** and then fill out the details **Activity Name** and **Layout Name** then click **Finish**. 135 | 136 | 4) For **Gradle** : You can add Retrofit library by adding it on **app =>build.gradle** (in project view). 137 | 138 | ![gradle-dependencies](http://img.my.csdn.net/uploads/201504/13/1428929609_9928.png) 139 | 140 | For Jar : Add jar to app => libs folder and right click on the jar file and click on Add as Library. 141 | 142 | 5) Also create two packages as **API** and **model**. 143 | 144 | 6) Right Click on **API** and Click **New** => **Java Class** ,then **Name** it as gitapi and Kind as Interface. 145 | 146 | 7) Right Click on Package model and Click **New** => **Java Class**,then Name it as gitmodel and Kind as Class. 147 | 148 | ![proj-structure](http://img.my.csdn.net/uploads/201504/13/1428929622_5732.png) 149 | 150 | ## 3) 创建项目 151 | 152 | 1) 在Android Studio中创建新项目: **File => New Project** ,填写描述信息并点击**Next**. 153 | 154 | 2) 填写**minimum SDK**,我用的是**4.0+**(Retrofit支持Android **2.3+**或Java **6**以上) 155 | 156 | 3) 选择**Blank Activity**然后填写**Activity Name**和**Layout Name**,最后点击 **Finish**. 157 | 158 | 4) **Gradle** : 你可以在**app =>build.gradle**中添加Retrofit的库文件。 159 | 160 | ![gradle-dependencies](http://img.my.csdn.net/uploads/201504/13/1428929609_9928.png) 161 | 162 | Jar包的添加方式 : 将jar包添加到libs文件夹下,右键点击Add as Library. 163 | 164 | 5) 创建两个包:**API**和**model**。 165 | 166 | 6) 在**API**包下右键点击**New** => **Java Class** , 填写**Name**为gitapi并设置为Interface。 167 | 168 | 6) 在**API**包下创建名gitapi接口。 169 | 170 | 7) 在**model**包下右键点击**New** => **Java Class**, 填写**Name**为gitmodel并设置为Class。 171 | 172 | ![proj-structure](http://img.my.csdn.net/uploads/201504/13/1428929622_5732.png) 173 | 174 | ## 4) Android Manifest 175 | 176 | 1) Add INTERNET PERMISSION 177 | 178 | ```xml 179 | 180 | ``` 181 | 182 | Your `Manifest` will look like : 183 | 184 | ```xml 185 | 186 | 188 | 189 | 193 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | ``` 204 | 205 | ## 4) Android Manifest 206 | 207 | 1) 添加INTERNET PERMISSION权限 208 | 209 | ```xml 210 | 211 | ``` 212 | 213 | 你的`Manifest`文件看起来应该是这样的 : 214 | 215 | ```xml 216 | 217 | 219 | 220 | 224 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | ``` 235 | 236 | ## 5) Model Class 237 | 238 | First,we need to create POJO or Model class.The Json from server cannot be use directly in Java,so we need to use model class. 239 | 240 | For URL structure look like this : https://api.github.com/users/ + “search term” 241 | 242 | For eg : https://api.github.com/users/basil2style 243 | 244 | Our Json Response look like this : 245 | 246 | ![json object response](http://img.my.csdn.net/uploads/201504/13/1428929583_4905.jpg) 247 | 248 | This is a **JSON Object**. If you don’t know the difference between Json Array and Json Object,please [read this](http://stackoverflow.com/questions/12289844/difference-between-jsonobject-and-jsonarray) 249 | 250 | Use [jsonschema2pojo](http://www.jsonschema2pojo.org/) for creating the pojo easily.Do not use this for every other Json requests,sometime it gives error. I use Source type as **Json** and Annotation style as **Gson**,then click on **preview**. 251 | 252 | gitmodel.java : http://pastebin.com/4xckerN1 253 | 254 | ## 5) 模型类 255 | 256 | 首先,我们需要创建一个POJO或模型类。服务器返回的JSON数据不能在Java里直接使用,所以我们需要用模型类来做转换。 257 | 258 | URL的结构是这样的:https://api.github.com/users/ + “search term” 259 | 260 | 举个栗子:https://api.github.com/users/basil2style 261 | 262 | 我们的JSON返回数据是这样的: 263 | 264 | ![json object response](http://img.my.csdn.net/uploads/201504/13/1428929583_4905.jpg) 265 | 266 | 这是一个**JSON Object**,如果你不了解**JSON Array**和**JSON Object**的区别,请看[这里](http://stackoverflow.com/questions/12289844/difference-between-jsonobject-and-jsonarray)。 267 | 268 | 使用[jsonschema2pojo](http://www.jsonschema2pojo.org/)来创建POJO更加简单,不要每一个JSON数据的POJO转换都用它,有时候会报错。选择源代码类型为**Json**,注解类型是**Gson**,然后点击**preview**。 269 | 270 | 点击[这里](http://pastebin.com/4xckerN1)(需翻墙)查看`gitmodel.java`源代码。 271 | 272 | ## 6) gitapi.java 273 | 274 | 1) Now we need url calling using our interface. 275 | 276 | **@GET(“/users/{user}”)**,this will call the server.where url is from after the BASE URL.The service calling url will start with ‘/’ and {user} is the string retrieved from edittext. 277 | 278 | **@Path(“user”) String user** is the string which we get from the edit text. 279 | 280 | response from the server is then saved into the Pojo. 281 | 282 | ```java 283 | public interface gitapi { 284 | 285 | @GET("/users/{user}") // here is the other url part.best way is to start using / 286 | public void getFeed(@Path("user") String user, Callback response); 287 | // string user is for passing values from edittext for eg: user=basil2style,google 288 | // response is the response from the server which is now in the POJO 289 | } 290 | ``` 291 | 292 | ## 6) gitapi.java 293 | 294 | 1) 现在我们需要使用接口调用URL. 295 | 296 | **`@GET("/users/{user}")`**, 添加这个注解会调用服务器,参数url基于BASE URL,服务调用的参数以'/'开头,其中{user}是从EditText获取的字符串。 297 | 298 | **`@Path("user") String user`** 就是我们从EditText获取的字符串。 299 | 300 | 服务器端响应的数据则会被存储到POJO实例中去。 301 | 302 | ```java 303 | public interface gitapi { 304 | 305 | @GET("/users/{user}") // here is the other url part.best way is to start using / 306 | public void getFeed(@Path("user") String user, Callback response); 307 | // string user is for passing values from edittext for eg: user=basil2style,google 308 | // response is the response from the server which is now in the POJO 309 | } 310 | ``` 311 | 312 | ## 7) RestAdapter 313 | 314 | Now,this is the main part.you need to setup the Rest Adapter and the service. 315 | 316 | 1) API is the Base URL. 317 | 318 | 2) We need to create a **`RestAdapter`** object with **`Endpoint(API)`** and then **`buid()`**. 319 | 320 | 3) Create a service for adapter with our **gitapi**. 321 | 322 | 4) Call the function for getting the response,Callback is used for async method.We need Callback for both success request and error handling. 323 | 324 | 5) Our parsed json will be now in POJO. You can call each by calling each item. 325 | 326 | ```java 327 | String API = "https://api.github.com"; 328 | 329 | RestAdapter restAdapter = new RestAdapter.Builder().setLogLevel(RestAdapter.LogLevel.FULL)setEndpoint(API).build(); 330 | 331 | gitapi git = restAdapter.create(gitapi.class); 332 | 333 | git.getFeed(user, new Callback() { 334 | @Override 335 | public void success(gitmodel gitmodel, Response response) { 336 | tv.setText("Github Name :" + gitmodel.getName() + 337 | "\nWebsite :" + gitmodel.getBlog() + 338 | "\nCompany Name :" + gitmodel.getCompany()); 339 | 340 | pbar.setVisibility(View.INVISIBLE); // disable progressbar 341 | } 342 | 343 | @Override 344 | public void failure(RetrofitError error) { 345 | tv.setText(error.getMessage()); 346 | pbar.setVisibility(View.INVISIBLE); //disable progressbar 347 | } 348 | }); 349 | ``` 350 | 351 | ## 7) RestAdapter 352 | 353 | 现在该主要部分了,你需要设置`Rest Adapter`和`service`类. 354 | 355 | 1) API就是`Base URL`. 356 | 357 | 2) 我们需要设置**`Endpoint(API)`**并调用**`buid()`**方法来创建一个**`RestAdapter`**对象。 358 | 359 | 3) 使用我们的**`gitapi`**来创建一个服务适配器(service for adapter)。 360 | 361 | 4) 调用函数并获得响应数据,回调接口是用来异步的获取模型实例的,我们的回调接口需要实现成功回调方法(success request)和错误处理方法(error handling)。 362 | 363 | 5) Our parsed json will be now in POJO. You can call each by calling each item. 364 | 365 | 5) 我们解析好的json数据的现在就存在于POJO实例中了,你可以每次调用一条。 366 | 367 | ```java 368 | String API = "https://api.github.com"; 369 | 370 | RestAdapter restAdapter = new RestAdapter.Builder().setLogLevel(RestAdapter.LogLevel.FULL).setEndpoint(API).build(); 371 | 372 | gitapi git = restAdapter.create(gitapi.class); 373 | 374 | git.getFeed(user, new Callback() { 375 | @Override 376 | public void success(gitmodel gitmodel, Response response) { 377 | tv.setText("Github Name :" + gitmodel.getName() + 378 | "\nWebsite :" + gitmodel.getBlog() + 379 | "\nCompany Name :" + gitmodel.getCompany()); 380 | 381 | pbar.setVisibility(View.INVISIBLE); // disable progressbar 382 | } 383 | 384 | @Override 385 | public void failure(RetrofitError error) { 386 | tv.setText(error.getMessage()); 387 | pbar.setVisibility(View.INVISIBLE); //disable progressbar 388 | } 389 | }); 390 | ``` 391 | 392 | 完整的`MainActivty.java`代码: 393 | 394 | ```java 395 | package com.makeinfo.flowerpi; 396 | 397 | import android.support.v7.app.ActionBarActivity; 398 | import android.os.Bundle; 399 | import android.view.Menu; 400 | import android.view.MenuItem; 401 | import android.view.View; 402 | import android.widget.Button; 403 | import android.widget.EditText; 404 | import android.widget.ProgressBar; 405 | import android.widget.TextView; 406 | 407 | import com.makeinfo.flowerpi.API.gitapi; 408 | import com.makeinfo.flowerpi.model.gitmodel; 409 | 410 | import retrofit.Callback; 411 | import retrofit.RestAdapter; 412 | import retrofit.RetrofitError; 413 | import retrofit.client.Response; 414 | 415 | 416 | public class MainActivity extends ActionBarActivity { 417 | 418 | Button click; 419 | TextView tv; 420 | EditText edit_user; 421 | ProgressBar pbar; 422 | String API = "https://api.github.com"; // BASE URL 423 | 424 | @Override 425 | protected void onCreate(Bundle savedInstanceState) { 426 | super.onCreate(savedInstanceState); 427 | setContentView(R.layout.activity_main); 428 | 429 | click = (Button) findViewById(R.id.button); 430 | tv = (TextView) findViewById(R.id.tv); 431 | edit_user = (EditText) findViewById(R.id.edit); 432 | pbar = (ProgressBar) findViewById(R.id.pb); 433 | pbar.setVisibility(View.INVISIBLE); 434 | 435 | click.setOnClickListener(new View.OnClickListener() { 436 | @Override 437 | public void onClick(View v) { 438 | String user = edit_user.getText().toString(); 439 | pbar.setVisibility(View.VISIBLE); 440 | 441 | // Retrofit section start from here... 442 | // create an adapter for retrofit with base url 443 | RestAdapter restAdapter = new RestAdapter.Builder().setEndpoint(API).build(); 444 | 445 | // creating a service for adapter with our GET class 446 | gitapi git = restAdapter.create(gitapi.class); 447 | 448 | // Now ,we need to call for response 449 | // Retrofit using gson for JSON-POJO conversion 450 | 451 | git.getFeed(user,new Callback() { 452 | @Override 453 | public void success(gitmodel gitmodel, Response response) { 454 | // we get json object from github server to our POJO or model class 455 | 456 | tv.setText("Github Name :" + gitmodel.getName() + 457 | "\nWebsite :"+gitmodel.getBlog() + 458 | "\nCompany Name :"+gitmodel.getCompany()); 459 | 460 | pbar.setVisibility(View.INVISIBLE); // disable progressbar 461 | } 462 | 463 | @Override 464 | public void failure(RetrofitError error) { 465 | tv.setText(error.getMessage()); 466 | pbar.setVisibility(View.INVISIBLE); // disable progressbar 467 | } 468 | }); 469 | } 470 | }); 471 | } 472 | 473 | 474 | @Override 475 | public boolean onCreateOptionsMenu(Menu menu) { 476 | // Inflate the menu; this adds items to the action bar if it is present. 477 | getMenuInflater().inflate(R.menu.menu_main, menu); 478 | return true; 479 | } 480 | 481 | @Override 482 | public boolean onOptionsItemSelected(MenuItem item) { 483 | // Handle action bar item clicks here. The action bar will 484 | // automatically handle clicks on the Home/Up button, so long 485 | // as you specify a parent activity in AndroidManifest.xml. 486 | int id = item.getItemId(); 487 | 488 | //noinspection SimplifiableIfStatement 489 | if (id == R.id.action_settings) { 490 | return true; 491 | } 492 | 493 | return super.onOptionsItemSelected(item); 494 | } 495 | } 496 | ``` 497 | -------------------------------------------------------------------------------- /androidweekly/Android测试框架:Dagger 2 + Espresso 2 + Mockito/README.md: -------------------------------------------------------------------------------- 1 | ## Android测试框架: Dagger 2 + Espresso 2 + Mockito 2 | (by [@Chiu-Ki Chan](http://www.blogger.com/profile/01970007638489793840)) 3 | --- 4 | 5 | > 6 | * 原文链接 : [Dagger 2 + Espresso 2 + Mockito](http://blog.sqisland.com/2015/04/dagger-2-espresso-2-mockito.html) 7 | * 译者 : [yaoqinwei](https://github.com/yaoqinwei) 8 | * 校对者: [](github链接) 9 | * 状态 : 未完成 10 | 11 | 12 | 13 | 14 | I've been doing Android instrumentation testing with Dagger, Espresso and Mockito, and I love it. To commemorate the launch of [Dagger 2](http://google.github.io/dagger/) out of SNAPSHOT, I am sharing a demo repo with Dagger 2, Espresso 2 and Mockito: 15 | 16 | https://github.com/chiuki/android-test-demo 17 | 18 | 我一直在用Dagger, Espresso和Mockito做Android测试,天呐,爱死这个组合了!为了庆祝[Dagger 2](http://google.github.io/dagger/)的推出,我分享了一个用Dagger 2, Espresso 2和Mockito做Android测试的Demo: 19 | 20 | https://github.com/chiuki/android-test-demo 21 | 22 | ### Dagger Components 23 | 24 | [Dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) allows our app and test obtain different modules. This is very useful for creating repeatable test cases. The demo app displays today's date in the format yyyy-MM-dd. We would like to test that against a known date, instead of depend on the actual date when we run the test. 25 | 26 | In Dagger 2, a Component provides modules for your whole app, and defines where to inject them. 27 | 28 | ```java 29 | public interface DemoComponent { 30 | void inject(MainActivity mainActivity); 31 | } 32 | 33 | @Singleton 34 | @Component(modules = ClockModule.class) 35 | public interface ApplicationComponent extends DemoComponent { 36 | } 37 | 38 | @Singleton 39 | @Component(modules = MockClockModule.class) 40 | public interface TestComponent extends DemoComponent { 41 | void inject(MainActivityTest mainActivityTest); 42 | } 43 | ``` 44 | 45 | **`ApplicationComponent`** is used when the app is run normally, and **`TestComponent`** is used during tests. Both components injects into **`MainActivity`**. 46 | 47 | How does the **`MainActivity`** know which component to use? It injects via the **`DemoApplication`**, which stores the component. 48 | 49 | ```java 50 | private DemoComponent component = null; 51 | 52 | @Override 53 | public void onCreate() { 54 | super.onCreate(); 55 | if (component == null) { 56 | component = DaggerDemoApplication_ApplicationComponent 57 | .builder() 58 | .clockModule(new ClockModule()) 59 | .build(); 60 | } 61 | } 62 | 63 | public void setComponent(DemoComponent component) { 64 | this.component = component; 65 | } 66 | 67 | public DemoComponent component() { 68 | return component; 69 | } 70 | ``` 71 | 72 | We call **`setComponent()`** in test, which runs before **`onCreate()`**, so the TestComponent is used. When the app is run normally, component will be set to **`ApplicationComponent`** in **`onCreate()`**. 73 | 74 | ### Dagger 组件(Components) 75 | 76 | [Dependency injection(依赖注入)](http://en.wikipedia.org/wiki/Dependency_injection) 允许我们在App开发和测试中可以获取到不同的模块,非常有利于创建可重用的测试用例,这个Demo App的功能是以`"yyyy-MM-dd"`格式显示今天的日期,我们需要测试一下来应对一些已知的日期,而非依赖于运行测试时的真实日期。 77 | 78 | 在`Dagger 2`中,一个组件(Component)接口可以给整个App提供模块,并且定义了在哪注入它们。 79 | 80 | ```java 81 | public interface DemoComponent { 82 | void inject(MainActivity mainActivity); 83 | } 84 | 85 | @Singleton 86 | @Component(modules = ClockModule.class) 87 | public interface ApplicationComponent extends DemoComponent { 88 | } 89 | 90 | @Singleton 91 | @Component(modules = MockClockModule.class) 92 | public interface TestComponent extends DemoComponent { 93 | void inject(MainActivityTest mainActivityTest); 94 | } 95 | ``` 96 | 97 | **`ApplicationComponent`**组件用于App的正常运行, 而**`TestComponent`**组件则用于测试,这两个组件都可以被注入到**`MainActivity`**中。 98 | 99 | **`MainActivity`**如何知道使用的哪个组件(component)? 答案是通过**`DemoApplication`**来注入, 它保存着该组件(component)的引用。 100 | 101 | ```java 102 | private DemoComponent component = null; 103 | 104 | @Override 105 | public void onCreate() { 106 | super.onCreate(); 107 | if (component == null) { 108 | component = DaggerDemoApplication_ApplicationComponent 109 | .builder() 110 | .clockModule(new ClockModule()) 111 | .build(); 112 | } 113 | } 114 | 115 | public void setComponent(DemoComponent component) { 116 | this.component = component; 117 | } 118 | 119 | public DemoComponent component() { 120 | return component; 121 | } 122 | ``` 123 | 124 | 测试时,我们需要在**`onCreate()`**方法执行之前调用**`setComponent()`**方法,将组件设置为**`TestComponent`**。而App正常运行时,组件在**`onCreate()`**方法中就被设置为**`ApplicationComponent`**了。 125 | 126 | ### Mockito 127 | 128 | The app has a **`Clock`** class which returns the current time. 129 | 130 | ```java 131 | public DateTime getNow() { 132 | return new DateTime(); 133 | } 134 | ``` 135 | 136 | **`TestComponent`** contains **`MockClockModule`**, which provides **`Clock`** as mocked by [Mockito](http://mockito.org/). This way [**`MainActivityTest`**](https://github.com/chiuki/android-test-demo/blob/master/app/src/androidTest/java/com/sqisland/android/test_demo/MainActivityTest.java) can supply a pre-determined date during test. 137 | 138 | ```java 139 | Mockito.when(clock.getNow()).thenReturn(new DateTime(2008, 9, 23, 0, 0, 0)); 140 | ``` 141 | 142 | Since we have singleton modules, the same mocked **`Clock`** is supplied to the app. With that, it will display the provided date instead of today's date: 143 | 144 | ```java 145 | onView(withId(R.id.date)).check(matches(withText("2008-09-23"))); 146 | ``` 147 | 148 | ### Mockito 149 | 150 | App中有一个**`Clock`**类,其中有一个方法可以返回当前的时间: 151 | 152 | ```java 153 | public DateTime getNow() { 154 | return new DateTime(); 155 | } 156 | ``` 157 | 158 | **`TestComponent`**组件中包含**`MockClockModule`**模块,后者使用[Mockito](http://mockito.org/)提供了一个模拟的**`Clock`**。这样[**`MainActivityTest`**](https://github.com/chiuki/android-test-demo/blob/master/app/src/androidTest/java/com/sqisland/android/test_demo/MainActivityTest.java)就可以在测试期间提供一个预先设置的日期了。 159 | 160 | ```java 161 | Mockito.when(clock.getNow()).thenReturn(new DateTime(2008, 9, 23, 0, 0, 0)); 162 | ``` 163 | 164 | 因为我们使用了单例, 相同的模拟**`Clock`**将为整个App提供日期,这样就能被显示提供的日期,而非今天的日期了: 165 | 166 | ```java 167 | onView(withId(R.id.date)).check(matches(withText("2008-09-23"))); 168 | ``` 169 | 170 | ### More 171 | 172 | There is a lot more in the repo, including testing activity launch with intent and unit testing with JUnit. Please check it out: 173 | 174 | https://github.com/chiuki/android-test-demo 175 | 176 | Further reading: 177 | 178 | > 179 | * Instrumentation Testing with Dagger, Mockito, and Espresso 180 | * A JUnit @Rule which launches an activity when your test starts 181 | * EspressoStartGuide 182 | * What’s new in Android Testing 183 | * https://github.com/googlesamples/android-testing 184 | 185 | ### 更多 186 | 187 | 这里还有很多示例, 包括使用intent启动的activity的测试和使用JUnit测试的单元测试,请点击下面链接查看: 188 | 189 | https://github.com/chiuki/android-test-demo 190 | 191 | 相关阅读: 192 | 193 | > 194 | * Instrumentation Testing with Dagger, Mockito, and Espresso 195 | * A JUnit @Rule which launches an activity when your test starts 196 | * EspressoStartGuide 197 | * What’s new in Android Testing 198 | * https://github.com/googlesamples/android-testing 199 | * 200 | 201 | 202 | 203 | 204 | // sample 205 | 206 | ```java 207 | Color[] colorRange = new Color[]{[255,60,0], [255,255,0]}, 208 | int boltCount = 2; 209 | Bolt eachBolt = new Bolt(); 210 | if(bolts.size == colorRange.size) { 211 | eachBolt.startColor = colorRange[boltIndex]; 212 | } 213 | ``` 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(1)/images/broken_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(1)/images/broken_gif.gif -------------------------------------------------------------------------------- /androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(1)/images/clipped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(1)/images/clipped.png -------------------------------------------------------------------------------- /androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(1)/images/clipping_fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(1)/images/clipping_fixed.png -------------------------------------------------------------------------------- /androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(1)/images/demo_gif (1).gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(1)/images/demo_gif (1).gif -------------------------------------------------------------------------------- /androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(1)/images/demo_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(1)/images/demo_gif.gif -------------------------------------------------------------------------------- /androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(1)/readme.md: -------------------------------------------------------------------------------- 1 | ListView或者RecycleView滚动时隐藏Toolbar (1) 2 | --- 3 | 4 | > 5 | * 原文链接 : [How to hide/show Toolbar when list is scroling (part 1)](http://mzgreen.github.io/2015/02/15/How-to-hideshow-Toolbar-when-list-is-scroling%28part1%29/) 6 | * 译者 : [chaossss](https://github.com/chaossss) 7 | * 校对者: [这里校对者的github用户名](github链接) 8 | * 状态 : 校对中 9 | 10 | 11 | 今天我打算写一篇博文给大家介绍Google+ App的一个酷炫效果——向上/向下滚动ListView/RecyclerView时,Toolbar和FAB(屏幕右下方的小按钮)会隐藏/出现。这个效果也被Google视为符合 Material Design 规范的效果哦,详情参见: [Material Design Checklist](http://android-developers.blogspot.com/2014/10/material-design-on-android-checklist.html) 。 12 | 13 | 14 | > 如果可以的话,我们希望当屏幕向下滚动时,App的Toolbar和FAB将离开屏幕,从而在垂直方向上为可显示区域腾出更大的空间来显示我们的内容。而当我们的屏幕向上滚动时,App的Toolbar和FAB会再次出现。 15 | 16 | 我们做出来的最终效果应该是下面这样的: 17 | 18 | ![](http://mzgreen.github.io/images/1/demo_gif.gif) 19 | 20 | 在这篇博文的讲解中,我们将会用RecyclerView作为我们的list,但这不代表其他可滚动的容器就不能实现这样的效果了,只是其他的可滚动容器(如:ListView)需要要多花一些功夫才能实现这个效果。那么我们要怎么去实现呢?我想到了两个办法: 21 | 22 | - 对list容器加一个Padding 23 | - 对list容器加一个Header 24 | 25 | 就我个人而言,我更想用第二种方法去实现,因为在设计代码的过程中我发现:为RecyclerView添加Header会产生几个问题,这给了我很好的机会去思考如何解决它,与此同时,在这个思考和解决问题的过程中我还能学习到更多的知识,何乐而不为呢?不过我还是会给大家简要地介绍如何使用第一种方法实现的啦! 26 | 27 | ## 那就让我们开始今天的讲解吧! 28 | 29 | 首先,我们需要创建一个工程和添加必要的工具库: 30 | 31 | 接着我们需要定义 styles.xml ,以确保我们的App没有添加Actionbar(因为我们将会使用Toolbar),同时我们App的设计风格要符合Google的 Material Design 规范。 32 | 33 | 最后我们就要创建我们activity中显示的布局: 34 | 35 | 事实上,我们只需要一个简单的布局,其中包含了:RecyclerView,Toolbar和ImageButton。但需要注意的是:我们需要把它们放在一个FrameLayout里,否则当我们隐藏Toolbar时list的上方将会出现一个空白区域,这显然不会是我们想要的效果。我们理想的效果应该是:当Toolbar被隐藏,list能在屏幕的可见区域中显示出一整个列表,而这就需要通过使Toolbar覆盖在RecycleView上面来实现。 36 | 37 | 接着来看看我们MainActivity代码吧: 38 | 39 | 就像你看到的那样,这是一个只重写了OnCreate()方法,非常非常非常简单的Activity。而且OnCreate()方法也只做了下面三件事情: 40 | 41 | 1. 初始化Toolbar 42 | 43 | 1. 获取mFabButton的引用(mFabButton是FAB的对象哦,也就是屏幕右下方的小按钮) 44 | 45 | 1. 初始化RecyclerView 46 | 47 | 为了在list中显示出内容,我们现在就要为RecyclerView创建一个Adapter啦。但在此之前,我们应该为我们list中的item添加相应的子布局以及对应的ViewHolder: 48 | 49 | list的每一个item只需要一个text用来显示文字,非常简单! 50 | 51 | 那RecyclerAdapter该怎么实现呢: 52 | 53 | 就像你看到的这样,这是一个非常普通的RecycleView.Adapter,没有任何特别的地方,是不是感觉被骗了呐 23333~(如果你想要了解更多有关RecyclerView知识,我强烈建议你去看大牛 Mark Allison's 巨巨写的这些优秀文章 [series of posts](https://blog.stylingandroid.com/material-part-4/)) 54 | 55 | 经过上面的努力,我们已经把车子的小零件组装的七七八八啦,是时候让小车子上路跑一跑,展现真正的技术了! 56 | 57 | ![](http://mzgreen.github.io/images/1/clipped.png) 58 | 59 | WTF,谁能告诉我这是什么鬼……? 60 | 61 | 我相信只要不是瞎子都会发现,App的Toolbar竟然把我们的item挡住了!!!或许部分机智的小伙伴已经发现了问题所在:出现这样的问题是因为我们在activity_main.xml里使用了FrameLayout,正是FrameLayout导致了这样的问题,而这就是我开头所提到的问题之一了。 62 | 63 | 第一个解决办法是为我们的RecyclerView添加一个PaddingTop,并将PaddingTop的值设置为Toolbar的高度。但有一个细节我们不能忽略,那就是RecyclerView会默认裁剪到子View的Padding区域,所以为了我们伟大的事业,我们必须把这个特性关掉。 64 | 65 | 经过这些修改之后,我们就能实现我们想要的效果,这就是我所说的第一种方法。但如我所说,我写这篇博文的目的,不仅仅只是教你实现这个效果,然后就完了。我想教给你实现同一个效果各种各样的方法,并且为你介绍其中的思想,让你接触到平常很难接触到的问题并教你如何解决它。有些方法固然会更加复杂(在本文中是为list添加一个Header),但你在实现过程中也能学到更多的知识,毕竟授人以鱼不如授人以渔嘛。 66 | 67 | ## 为RecycleView添加一个Header 68 | 69 | 70 | 要用第二种方法去实现这个效果,首先我们要做的就是稍微修改一下我们的Adapter: 71 | 72 | 下面是其实现原理: 73 | 74 | 我们需要定义一个常量来区分Recycler展现的item的类型。这里我需要为你介绍的是:RecyclerView是一个非常灵活的组件,RecyclerView 完全能实现你想要让list的item具有各种各样不同的布局的愿望,而此时,我们定义来区分item类型的常量就会被利用到。而这样的特性正是我们想要的——让Header成为RecyclerView的第一个item,显然这会与其余的item不一样。(第3-4行) 75 | 76 | 因而我们需要让Recycler知道它需要展示的子布局是什么类型的,在本文中我们用作类型区分的常量则是TYPE_ITEM和TYPE_HEADER。(第49-54行) 77 | 78 | 接着,我们需要修改onCreateViewHolder()和onBindViewHolder()方法,如果item的类型是TYPE_ITEM的话,使它们返回和绑定一个普通的item;如果item的类型是TYPE_HEADER的话,则返回Header。(第14-34行) 79 | 80 | 此外,由于我们的list并不仅仅只有普通的item,我们还在list中添加了Header,因此我们需要修改getItemCount()方法的返回值,让我们的返回值是普通item的总数量 + 1(第43-45行) 81 | 82 | 最后让我们来为Header布局创建一个layout和一个ViewHolder,但出乎意料的是,我们需要为Header创建的layout和ViewHolder都非常简单,唯一需要注意的是:Header的高度必须和Toolbar的高度一致。 83 | 84 | 那么这样我们就把布局弄好啦~不信你看图! 85 | 86 | ![](http://mzgreen.github.io/images/1/clipping_fixed.png) 87 | 88 | 所以总的来说,我们为RecyclerView添加了一个和Toolbar有相同高度的Header,而现在我们的Toolbar把header隐藏起来了(因为header现在是一个空的view),同时,我们所有的普通item都是可见的。那么现在就让我们来实现滚动时改变Toolbar和FAB的出现和隐藏吧! 89 | 90 | ## 滚动时控制Toolbar和FAB的出现和隐藏 91 | 92 | 93 | 为了实现这个效果,我们为RecyclerView再创建一个——OnScrollListener类就够了你敢信? 94 | 95 | 我现在还要告诉你,在OnScrollListener类里,我们只需要重载onScrolled()方法就能让这个酷炫的效果成为App中秒杀用户的黑魔法!其中,onScrolled()方法的参数——dx,dy分别是水平和垂直方向上的滚动距离。但大家需要注意的是:这里dx,dy并不是代表屏幕上的物理距离,而是两个事件的相对距离。 96 | 97 | 具体的实现算法大体如下: 98 | 99 | 只有当list向上滚动且Toolbar和FAB被隐藏,抑或是当list向下滚动且Toolbar和FAB出现,我们才会计算总的滚动距离,因为这两种情况下的滚动距离才是我们实现这个效果所需要关心的。 100 | 101 | 总的滚动距离需要超过我们展现/隐藏Toolbar和FAB所在的方向的极限值才能将其展现/隐藏(你把极限值调整的越大,通过滚动展示/隐藏Toolbar和FAB需要的距离就越大)(dy>0意味着我们在向下滚动,dy<0意味着我们在向上滑动) 102 | 103 | 实际上我们并没有在我们的滚动监听类里面展现/隐藏Toolbar和FAB,我们是通过调用show()/hide()方法来展现/隐藏Toolbar和FAB的,所以调用者可以通过接口实现它。 104 | 105 | 现在我们需要为RecyclerView添加它的监听: 106 | 107 | 下面是我们通过动画改变我们的视图的方法: 108 | 109 | 当我们隐藏Toolbar和FAB的时候,我们必须把Padding也考虑进去,不然的话视图并不能够完全被隐藏。 110 | 111 | 是骡子是马,让我们拉出来溜一溜! 112 | 113 | ![](http://mzgreen.github.io/images/1/broken_gif.gif) 114 | 115 | 虽然现在看起来已经很nice了,但其实这里有一个小小的bug——如果你在list的顶部,此时临界值非常小,因而你能隐藏Toolbar,但你在list的顶部会看到有一个空白的区域。不过幸好这里有一个很简单的方法可以解决这个bug:我们可以通过检测当前list的第一个item是否为可见的,只有当它不可见,才使用我们设计的展示/隐藏逻辑。 116 | 117 | 在这样的修改后,即使第一个item是可见的并且有item被它挡住了,我们也在展示它们,除此以外的情况我们都像之前说的那样实现我们的效果。 118 | 119 | 各位观众,接下来,就是见证奇迹的时刻: 120 | 121 | ![](http://mzgreen.github.io/images/1/demo_gif.gif) 122 | 123 | 太棒了思密达!感觉之前的失败都如雨后甘霖温润我脆弱的心灵呐~ 124 | 125 | 其实羞羞地说一句……这篇文章是我人生中的第一篇博文呐,如果你觉得很无聊或者我有哪里讲解错误的话千万不要喷我哦。我会在未来变得更棒哒,然后尽我所能为大家贡献更多的文章! 126 | 127 | 如果你看到这里还是通过添加Header来实现这个效果很恼火的话,用第一种方法结合HidingScrolllistener 也是可以实现这个效果的~ 128 | 129 | 如果你有什么疑问的话,可以在评论区问我哦,我都会尽我所能为你解答的! 130 | 131 | ## 源码 132 | 133 | 整个项目的源码在GitHub上面都有,大家可以在这看 [repo](https://github.com/mzgreen/HideOnScrollExample) 134 | 135 | 感谢Mirek Stanek帮我校对文章,么么哒!爱你的好基友Michal Z~ 136 | 137 | 如果你喜欢这篇博文的话,你可以[在Twitter上分享给你的小伙伴](https://twitter.com/intent/tweet?url=http://mzgreen.github.io/2015/02/15/How-to-hideshow-Toolbar-when-list-is-scroling(part1)/&text=How%20to%20hide/show%20Toolbar%20when%20list%20is%20scroling%20(part%201)&via=mzmzgreen)或者[在Twitter上关注我哦](https://twitter.com/mzmzgreen)! 138 | -------------------------------------------------------------------------------- /androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(2)/images/goal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(2)/images/goal.gif -------------------------------------------------------------------------------- /androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(2)/images/nosnap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(2)/images/nosnap.gif -------------------------------------------------------------------------------- /androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(2)/images/playstore.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(2)/images/playstore.gif -------------------------------------------------------------------------------- /androidweekly/ListView或者RecycleView滚动时隐藏Toolbar(2)/readme.md: -------------------------------------------------------------------------------- 1 | ListView或者RecycleView滚动时隐藏Toolbar( Part 2 ) 2 | --- 3 | 4 | > 5 | * 原文链接 : [How to hide/show Toolbar when list is scrolling (part 2)](http://mzgreen.github.io/2015/02/28/How-to-hideshow-Toolbar-when-list-is-scrolling(part2)/) 6 | * 译者 : [chaossss](https://github.com/chaossss) 7 | * 校对者: [这里校对者的github用户名](github链接) 8 | * 状态 : 校对中 9 | 10 | 11 | 12 | 13 | 14 | Hello,各位小伙伴,俺胡汉三又来了!!!今天我打算接着上一篇博文继续给大家讲解展现/隐藏Toolbar的效果。我建议没有读过 [ListView或者RecycleView滚动时隐藏Toolbar](https://github.com/bboyfeiyu/android-tech-frontier/blob/master/androidweekly/ListView%E6%88%96%E8%80%85RecycleView%E6%BB%9A%E5%8A%A8%E6%97%B6%E9%9A%90%E8%97%8FToolbar/readme.md) 这篇文章的小伙伴先去看看那篇博文再来看这篇博文,不然会跟不上我的讲解节奏的哦。在上一篇博文里,我们学习了如何去实现Google+那个酷炫的展现/隐藏Toolbar的效果,今天,我将会给大家讲解怎么让上一篇博文的效果进化成Google Play Store Toolbar那样,废话不多说,我们现在进入正题吧: 15 | 16 | 在我们开始之前,我想先告诉大家的是我已经对这个项目进行了一些重构——我继承项目的 MainActivity 实现了两个新的子类:PartOneActivity 和 PartTwoActivity。源码在包 partone 和 parttwo里,你可以在这两个包里挑选你喜欢的那一个使用 17 | 18 | 下面是我们的最终效果图,我把它和 Google Play Store Toolbar 放在一起比较,大家可以感受一下: 19 | 20 | ![](http://mzgreen.github.io/images/2/goal.gif) 21 | ![](http://mzgreen.github.io/images/2/playstore.gif) 22 | 23 | ## 准备工作 ## 24 | 25 | 在这里我不会再给大家展示 build.gradle 文件,因为这和第一部分的 build.gradle 文件是一样的,所以我们将会从这个步骤开始——为我们的Activity创建一个layout: 26 | 27 | 同样的,layout里面只有一个 RecyclerView 和一个 Toolbar (稍后再加上 Tabs)。值得注意的是,我这里的实现是使用之前说的第二种方法(为RecyclerView添加Padding) 28 | 29 | 同样的道理,由于我们的布局文件、list、RecyclerAdapter 都和之前是一样的,我在这里都不会再给大家讲解了。 30 | 31 | 那现在我们来看看 PartTwoAcitivty 的代码吧: 32 | 33 | 在 PartTwoActivty 里面仍然是简单地对 RecyclerView 和 Toolbar 进行初始化,但大家一定要记得设置 OnScrollListener 哦(第27行) 34 | 35 | 感觉大家看到这里也感觉昏昏欲睡了,因为前面提到的内容大体上都和上一篇相似。但是莫慌!俺接下来就要讲这篇博文中最有趣的部分 —— HidingScrollListener 了,请大家紧紧抱住我,跟上节奏! 36 | 37 | 如果你看过第一篇博文可能会觉得此情此景很熟悉(可能还会感觉简单一些)。我们在 HidingScrollListener 里耍了什么 tricks 呢?那就是存了与 Toolbar 的高度相关联的屏幕滚动偏移量 —— mToolbarOffset。为了简化其中的逻辑,只有当 mToolbarOffset 的取值在[0 , Toolbar的高度]之间时,我们才会实现我们的逻辑:当我们向上滚动时,mToolbarOffset的值会增加(但不会超过Toolbar的高度);当我们向下滚动时,mToolbarOffset 的值会减少(但不会低于0)。大家现在可能会有许多疑问:为什么要引用 mToolbarOffset 呀?为什么要让 mToolbarOffset 的取值范围介于那两者之间呐?别怕!你马上就会理解为什么我们要这样限制mToolbarOffset的取值了。我们必须知道的是,尽管我们极力去避免意外的发生,但现实总会出人意料,在这里也不例外,有时候 mToolbarOffset 的值就是会不可避免地在我们的取值范围之外,但由于我们的逻辑设计的限制,最终的显示效果会是闪烁一下。(例如:在屏幕上快速的挥动、滑动)这样的结果显然不是我们这些有格调的 Android 工程师想要的,因而我们需要对 mToolbarOffset 进行一定程度的裁剪,以规避这样的风险。基于这样的考虑,我们重载了 onMoved()方法 —— onMoved() 方法是一个当我们滚动视图时被调用的抽象方法。可能会吓到你,但是莫慌,继续抱住我! 38 | 39 | 接下来,我们就要回到我们 PartTwoActivity 设计之中,并且在我们的滚动监听器中实现 onMoved()方法。 40 | 41 | 是的,这就是所有内容啦。我们运行 App 后可以看到我们的最终效果: 42 | 43 | ![](http://mzgreen.github.io/images/2/nosnap.gif) 44 | 45 | 是不是感觉自己棒棒哒~就像我们想象的那样,Toolbar 完美的随着list的滚动实现了展现/隐藏的效果。其中的功劳都得归功于我们对 mToolbarOffset 取值范围的限制。如果我们省略掉检查 mToolbarOffset 是否在[0 , Toolbar的高度]范围中取值的过程,带着完成控件的喜悦向上滚动我们的list,Toolbar 确实会如你所期望的那样离开屏幕,但与此同时,Toolbar 还会远远地,远远地,离开视图,再也不回来。然后当你满是期许地向下滚动时,你就会发现刚刚那一声再见,竟是永远。如果你想再次遇见它,你就必须想下滚动,直到 mToolbarOffset = 0。 46 | 47 | 再脑补第二种情况吧,现在你正好把 Toolbar 滚动到 mToolbarHeight = mToolbarOffset 的位置,不偏不倚。那么现在Toolbar就刚好“坐”在了list顶部,如果你向上滚动的话,无论你怎么滚,它都不会动,只会静静地坐在那儿笑看人世沧桑。而如果你向下滚动,它又成为许多年前那个明亮、可爱的小女孩了。 48 | 49 | 虽然最终的实现效果看起来非常赞,但这并不是我想要的。因为我们能在滚动过程中停止整个效果,使得 Toolbar 有一部分是可见的,另一部分又是不可见的。但悲伤的是,Google Play Games 就是这么干的,而我一直认为这是一个Bug…… 50 | 51 | ## 在某一点停下 Toolbar ## 52 | 53 | 就我的认知来说,我认为滚动的Views是能够如丝般顺滑地对齐相应的位置的,就像 Chrome 应用里的 Logo/SearchBar 又或者是 Google Play Store应用里那样。我很确定我在 Material Design 的guidelines/checklist 或者是 以前听过的Google I/O 大会上听过类似的规范。 54 | 55 | 那我们现在再来看看 HidingScrollListener 的代码吧: 56 | 57 | 虽然为了实现上面提到的效果我们会让 HidingScrollListener 的代码变得更复杂一些,但是我再说一次,莫慌,抱紧我!我们现在只需要重载 RecyclerView.onScrollListener 类的 onScrollStateChanged()方法,然后按照下面那样干就行了: 58 | 59 | 首先,我们需要检查list是否处于 RecyclerView.SCROLL_STATE_IDLE 状态,以确保list没有在滚动或者挥动(因为如果list如果正在滚动或者挥动的时候,我们就需要像第一篇博文那样去考虑 Toolbar 的Y方向位置哦) 60 | 61 | 如果我们抬起了手指,而且list已经停止移动了()我们就要去检查Toolbar是不是可见的,如果是可见的,我们就需要考虑 mToolbarOffset 的值了。如果此时 mToolbarOffset 的值大于 HIDE_THRESHOLD 我们就必须把 Toolbar 隐藏起来;mToolbarOffset 的值小于 HIDE_THRESHOLD,我们则需要让 Toolbar 显示出来。 62 | 63 | 如果 Toolbar 不是可见的,我们就需要做相反的事情 —— 如果 此时mToolbarOffset(此时计算 mToolbarOffset要从顶部位置来考虑了,也就是 mToolbarHeight - mToolbarOffset) 大于 SHOW_THRESHOLD 我们就让 Toolbar显示;相反,如果 mToolbarOfffset 小于 SHOW_THRESHOLD ,我们就要再次将 Toolbar隐藏起来。 64 | 65 | onScrolled()方法和第一篇博文的一样,我们并不需要作什么改变。我们现在最后需要做的就是在 PartTwoActivity 类里实现我们的两个抽象方法: 66 | 67 | 那么,你准备好看魔术了吗? 68 | 69 | ![](http://mzgreen.github.io/images/2/snapnotabs.gif) 70 | 71 | hey,派大星你看,是不是很酷! 72 | 73 | 现在你可能在脑补添加 Tabs 会有多麻烦了,可是兄弟啊,生活很多时候都是出人意料的呐,不信你继续往下看呀 74 | 75 | ## 添加 Tabs ## 76 | 77 | 为了添加 Tabs,首先要做的当然是为我们的 Activity 布局添加一个 tabs.xml啦 78 | 79 | 你可以从源码那发现,我并没有添加真正的 Tabs,只是在布局里面模拟了 Tabs。而以上的一切不会改变任何之前的实现,你能在里面放任何你想要放的View。下面是一些 GitHub 上符合 Material Design规范的 Tabs 实现,当然你也可以自己实现啦。 80 | 81 | 添加 Tabs 意味这他们会稍微覆盖我们的list,所以我们需要增加Padding。为了减少代码的操作复杂度,我们不会在 xml 里进行这个操作(注意把 RecyclerView 在 part_tuo_activity里的padding删掉哦),因为 Toolbar 可能会在不同的设备中切换方向时拥有不同的高度(例如在平板中),这样的话我们需要创建一大堆的 xml 去解决这些乱七八糟的烦人问题。所以我决定在代码中解决这个问题:这是非常简单的,我们只需要和 Tabs高度的总和。如果我们把 Padding设置为 Toolbar 的高度现在运行起来的话,就会发现这样的东西: 82 | 83 | ![](http://mzgreen.github.io/images/2/withtabs.png) 84 | 85 | 看起来很正常的样子……我们第一个item刚刚好是可见的,我们也能移动跟随着它。实际上我们在 HidingScrollListener 类里什么也没干,唯一需要的改变都是在 PartTwoActivity 里做的: 86 | 87 | 你能发现什么发生了改变吗?我们现在不妨创建一个 mToolbarContainer 的引用,但是大家要注意哦, mToolbarContainer 是 LinearLayout 对象而不是 Toolbar对象,而且在 onMove(),onHide(),和 onShow()方法中,我们都把 Toolbar 改成了 mToolbarContainer。这会使得包含了 Toolbar 和 Tabs 的Container被移动,这恰恰就是我们想要做的。 88 | 89 | 如果我们把修改后的代码运行起来会发现,实际的运行效果正好就是我们所期望的,但如果你看的认真一些你会发现,里面其实有一个小Bug。在 Tabs 和 Toolbar 之间有时候会有一条白线,虽然时间非常短,但还是很惹人讨厌呐。我个人觉得这大概是因为当Toolbar 和 Tabs被显示的时候,他们并没有像我们期望的那样同步在一起。不过万幸这不是什么无法解决的Bug~ 90 | 91 | 解决办法非常简单,就是让 Toolbar 和 Tabs 的背景和父布局保持一致: 92 | 93 | 现在即使 mToolbarContainer 在显示过程中没有很好的同步在一起,白线也不会出现了。正当我打算吃根辣条庆祝我们伟大战役的胜利的时候,又出现了一个Bug………………这个Bug和我们在第一篇博文里遇到的Bug是一样的,如果我们在list的顶部,我们可以向上滚动一丢丢,如果此时 HIDE_THRESHOLD 足够小,Toolbar 就会藏起来,导致那里有一块空白区域(其实就是我们设置的Padding)在list的顶部,但是我相信你到了现在应该不会慌了,因为你已经知道所有Bug在我眼里都是非常容易解决的: 94 | 95 | 我们只需要再增加一个变量用于存储list的总滚动偏移量,当我们准备去检查我们是否应该展现/隐藏 Toolbar的时候,我们首先应该检查我们的滚动距离是否比 Toolbar 的高度要大(如果不是的话,我们再让 Toolbar 出现) 96 | 97 | 这就是今天博文要讲的一切了,让我们来看一看实际效果! 98 | 99 | ![](http://mzgreen.github.io/images/2/goal.gif) 100 | 101 | 现在运行的效果简直完美啊大兄弟~即使用其他的 LayoutManagers 也不需要改变任何东西的哦: 102 | 103 | ![](http://mzgreen.github.io/images/2/grid.png) 104 | 105 | 评论区有好学的同学问了个有关存储滚动状态的问题,这确实是个小麻烦。如果我们list的item中的文字在垂直方向达到2行,在水平方向达到1行的话,我们的item高度就会变得很奇怪了……举个例子吧,如果我们滚动到垂直方向100的位置,然后旋转我们的设备,同时存储 mTotalScrolledDistance的值,在旋转之后,我们会滚动到list的顶部,然后我们会发现 mTotalscrolleddistance 的值不等于0。这个时候即使全能如我也想不到简单的办法来解决这个问题了,但是在我们的使用场景中,这样的小问题并不会有什么影响。如果你真的想要解决这个问题的话,我个人的做法是:在旋转之后把 mTotalScrolledDistance 的值重设为0 并且显示 Toolbar。 106 | 107 | 感觉今天写了好多内容啊,大家看到这里应该感觉很累了吧?不过今天这篇博文就是这个系列的最后一篇文章啦,大家能在第一篇博文中学习到知识我真的很高兴呢。大家鼓励和夸奖的话也让我很感动,我会继续写我的博客,为大家传授更多的知识,不过我也不知道下一篇博文会在什么时候写 2333。 108 | 109 | 除此以外我还想说的是,在这两篇博文中我提到的方法可能看起来运行的很好,但其实我并没有进行非常严谨的测试,所以我也不确定它们能不能被用于企业级应用中(你看我们不就遇到了好几个Bug了嘛)。这个系列的博文的初衷只是想告诉你,即使只使用标准API里面的一两个简单方法,也能够实现酷炫的效果。同时,我在写博文的过程中也发现了这些方法还有其他有趣的用法(例如:利用视差背景制作有粘性的 Tabs,就像在 Google+ 个人页面那样)。不管怎样,祝大家在写代码的过程中找到更多的快乐! 110 | 111 | ## 源码 ## 112 | 113 | 整个项目的源码都已经被上传到 [GitHub](https://github.com/mzgreen/HideOnScrollExample) ,大家可以去下载和使用哦,爱你们的 Michal Z。 114 | 115 | 如果你喜欢这篇博文的话,你可以 [在Twitter上分享给你的小伙伴](https://twitter.com/intent/tweet?url=http://mzgreen.github.io/2015/02/28/How-to-hideshow-Toolbar-when-list-is-scrolling(part2)/&text=How%20to%20hide/show%20Toolbar%20when%20list%20is%20scrolling%20(part%202)&via=mzmzgreen) 或者 [在Twitter上关注我哦](https://twitter.com/mzmzgreen) 。 -------------------------------------------------------------------------------- /androidweekly/readme.md: -------------------------------------------------------------------------------- 1 | AndroidWeekly.net 2 | --- 3 | 4 | -------------------------------------------------------------------------------- /androidweekly/一个支持多设备的Android参考应用/readme.md: -------------------------------------------------------------------------------- 1 | 一个支持多设备的Android参考应用 2 | --- 3 | > 4 | * 原文链接 : [a-new-reference-app-for-multi-device](http://android-developers.blogspot.com/2015/03/a-new-reference-app-for-multi-device.html) 5 | * 译者 : [Mr.Simple & Sophie.Ping](https://www.github.com/bboyfeiyu) 6 | 7 | 现在你可以把你的app的好用之处分享给你的用户,不管他们身处何地,手上拿着何种设备。今儿我们会发布一个参考示例,展示一下如何把这种服务运用到一个在多个Android form-Factor运作的app上。这个示例叫做Universal Music Player,它是一个准系统但是参考功能齐全,在单个代码库里支持多种设备和形式因素。它能与Android Auto、Android Wear和Google Cast设备兼容。你可以试试把你的app适配到你的用户,无论他在哪里,无论他手上拿的是手机,手表,电视,汽车还是其他的设备。 8 | ![android](http://img.blog.csdn.net/20150322112056022) 9 | 10 | 锁屏时的播放控制和专辑封面 11 | 应用toolbar上的Google Cast 图标 12 | 13 | ![auto](http://img.blog.csdn.net/20150322111453169) 14 | 使用Android Auto控制播放 15 | 16 | ![watch](http://img.blog.csdn.net/20150322111518047) 17 | 通过Android Wear Watch控制播放 18 | 19 | 20 | 本示例运用了Android 5.0 Lollipop的几个新功能,比如MediaStyle通知,MediaSession和MediaBrowserService.这几个新功能使得在多个设备上使用单一app版本操作媒体浏览和回放变得更容易。 21 | 一起来看看源代码吧,让你的用户以他们的方式爱上你的app。 22 | 作者:Renato Mangini,高级开发工程师,Google开发者平台团队成员 -------------------------------------------------------------------------------- /androidweekly/一种在android中实现MVP模式的新思路/Android MVP - An Alternate Approach.md: -------------------------------------------------------------------------------- 1 | 一种在android中实现MVP模式的新思路 2 | --- 3 | > * 原文链接 : [android-mvp-an-alternate-approach](http://blog.cainwong.com/android-mvp-an-alternate-approach/) * 译者 : [FTExplore](https://github.com/FTExplore) * 校对 : [sundroid](https://github.com/sundroid) 4 | 今天我想分享我自己在Android上实现MVP模式的方法。如果你对MVP模式还不熟悉或者你不清楚为什么要在Android应用中使用MVP模式,我建议你先阅读以下[这篇维基百科的文章](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter)和[这篇博客](http://antonioleiva.com/mvp-android/)。 ## 使用Activity和Fragment作为视图层(View)真的合适么? 目前很多使用了MVP模式的android 项目,基本上都是将activity和fragment作为视图层来进行处理的.而presenters通常是通过继承自被视图层实例化或者注入的对象来得到的. 诚然,我同意说,这种方式可以节省掉很多让人厌烦的"import android.*.*"语句, 并且将presenters从activity的生命周期中分割出来以后, 项目后续的维护会变得简便很多.这种思路是正确的, 但是,从另一个角度来说, activity 有一个很复杂的生命周期(fragment的生命周期可能会更复杂), 而这些生命周期很有可能对你项目的业务逻辑有非常重大的影响. Activity 可以获取上下文环境和多种android系统服务. Activity中发送Intent,启动Service和执行Fragment事物等。而这些特性在我看来绝不应该是视图层应该涉及的领域(视图的功能就是现实数据和从用户那里获取输入数据,在理想的情况下,视图应该避免业务逻辑). 基于上述的原因,我对对目前的主流做法并不赞同,所以我在尝试使用Activity和Fragment作为Presenters。 ## 使用Activity和Fragment作为presenters的步骤 ### 1. 去除所有的view 5 | 将Activity和Fragment作为presenter最大的困难就是如何将关于UI的逻辑抽取出来.我的解决方案是: 让需要作为presenter的activity 或者 fragment来继承一个抽象的类(或者叫"基类"), 这样关于View 各种组件的初始化以及逻辑,都可以在继承了抽象类的方法中进行操作,而当继承了该抽象类的class需要对某些组件进行操作的时候,只需要调用继承自抽象类的方法,就可以了。 6 | 那么抽象类怎么获取到的view组件呢?在抽象类里面会有一个实例化的接口,这个接口里面的init()方法就会对view进行实例化,这个接口如下: ```java public interface Vu { void init(LayoutInflater inflater, ViewGroup container); View getView(); } ``` 7 | 正如你所见,Vu定义了一个通用的初始化教程,我可以通过它来实现一个容器视图,它也有一个方法来或各一个View的实例,每一个presenter将会和它自己的Vu关联,这个presenter将会继承这个接口(直接或者间接的去继承一个来自Vu的接口) ### 2. 创建一个presenter基类 (Activity) 8 | 有了Vu接口,我们可以通过构建一系列的class来操纵很多不同的view组件,这些class 使用Vu接口来初始化View组件,并通过继承的方式给子类以操纵view组件的方法,以此来达到将ui 逻辑剥离出activity的目的。在下面的代码中,你可以看到,我覆写了activity的onCreate 、 onCreateView、onDestroy 、 onDestroyView,通过对这些方法的覆写,就可以对Vu的实例化和销毁进行精确的控制(vu.init()就是实例化一个view组件)。onBindVu() 和onDestoryVu()是控制view生命周期的两个方法。通过对actiivty中相关方法的覆写达到控制组件的生命周期的目的(具体看下面的代码,你就明白了), 这样做的好处就是无论是activity 还是 fragment, 其用与控制view组件创建和销毁的语句是一样的(尽量避免定义多余的函数)。这样的话,二者之间的切换也会减少一定的阻力(也许你今天的需求是用fragment实现的,但是第二天发现使用fragment会有一个惊天bug,译者本人就遇到过)。 9 | ```java public abstract class BasePresenterActivity extends Activity { 10 | 11 | protected V vu; 12 | 13 | @Override 14 | protected final void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | try { 17 | vu = getVuClass().newInstance(); 18 | vu.init(getLayoutInflater(), null); 19 | setContentView(vu.getView()); 20 | onBindVu(); 21 | } catch (InstantiationException e) { 22 | e.printStackTrace(); 23 | } catch (IllegalAccessException e) { 24 | e.printStackTrace(); 25 | } 26 | } 27 | 28 | @Override 29 | protected final void onDestroy() { 30 | onDestroyVu(); 31 | vu = null; 32 | super.onDestroy(); 33 | } 34 | 35 | protected abstract Class getVuClass(); 36 | 37 | protected void onBindVu(){}; 38 | 39 | protected void onDestroyVu() {}; 40 | 41 | } 42 | ``` 43 | ### 3. 创建一个基本的presenter(Fragment) 44 | ```java public abstract class BasePresenterFragment extends Fragment { 45 | 46 | protected V vu; 47 | 48 | @Override 49 | public void onCreate(Bundle savedInstanceState) { 50 | super.onCreate(savedInstanceState); 51 | } 52 | 53 | @Override 54 | public final View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 55 | View view = null; 56 | try { 57 | vu = getVuClass().newInstance(); 58 | vu.init(inflater, container); 59 | onBindVu(); 60 | view = vu.getView(); 61 | } catch (java.lang.InstantiationException e) { 62 | e.printStackTrace(); 63 | } catch (IllegalAccessException e) { 64 | e.printStackTrace(); 65 | } 66 | return view; 67 | } 68 | 69 | @Override 70 | public final void onDestroyView() { 71 | onDestroyVu(); 72 | vu = null; 73 | super.onDestroyView(); 74 | } 75 | 76 | protected void onDestroyVu() {}; 77 | 78 | protected void onBindVu(){}; 79 | 80 | protected abstract Class getVuClass(); 81 | 82 | } 83 | ``` 84 | ### 4. 一个简单的例子 如前文所述,我们已经确定了一个框架,现在就来写一个简单的例子来进一步的说明. 为了避免篇幅过长,我就写一个“hello world”的例子。首先要有一个实现Vu接口的类: 85 | ```java public class HelloVu implements Vu { View view; TextView helloView; @Override public void init(LayoutInflater inflater, ViewGroup container) { view = inflater.inflate(R.layout.hello, container, false); helloView = (TextView) view.findViewById(R.id.hello); } @Override public View getView() { return view; } public void setHelloMessage(String msg){ helloView.setText(msg); } } 下一步,创建一个presenter来操作这个TextView public class HelloActivity extends BasePresenterActivity { @Override protected void onBindVu() { vu.setHelloMessage("Hello World!"); } @Override protected Class getVuClass() { return HelloVu.class; } } 86 | ``` OK,这样就大功告成了!!是不是很简便! 87 | ### 等等...耦合警告! 88 | 你可能注意到我的HelloVu类直接实现了Vu接口,我的Presenter的getVuClass方法直接饮用了实现类。传统的MVP模式中,Presenter是要通过接口与他们的View解耦合的。因此,你也可以这么做。避免直接实现Vu接口,我们可以创建一个扩展了Vu的IHelloView接口,然后使用这个接口作为Presenter的泛型类型。这样Presenter看起来应该是如下这样的 : 89 | 90 | ```java 91 | public class HelloActivity extends BasePresenterActivity { 92 | 93 | @Override 94 | protected void onBindVu() { 95 | vu.setHelloMessage("Hello World!"); 96 | } 97 | 98 | @Override 99 | protected Class getVuClass() { 100 | return HelloVuImpl.class; 101 | } 102 | } 103 | ``` 104 | 105 | 在我使用强大的模拟工具过程中,我个人并没有看到在一个接口下面实现Vu所带来的好处。但是对于我来说一个号的方面是,有没有Vu接口它都能够工作,唯一的需求就是最终你会实现Vu。 ## 5. 如何进行测试 通过以上几步,我们可以发现,去除了UI逻辑之后,Activity变得非常简洁。并且,相关的测试 也变的非常异常的简单。请看如下的代码: ```java public class HelloActivityTest { HelloActivity activity; HelloVu vu; @Before public void setup() throws Exception { activity = new HelloActivity(); vu = Mockito.mock(HelloVu.class); activity.vu = vu; } @Test public void testOnBindVu(){ activity.onBindVu(); verify(vu).setHelloMessage("Hello World!"); } } 106 | ``` 107 | 以上代码是一段标准的jnuit单元测试的代码,不需要在android设备中部署运行,只需要在编译环境中即可测试。大幅度的提高了测试效率。但是,在测试某些方法的时候,你必须要使用android设备,例如当你想测试activity生命周期中的resume()方法。在缺乏设备环境的时候,super.resume()会报错。为了解决这个问题,可以借鉴一些工具,例如Robolectric、还有android gradle 1.1 插件中内置的testOptions { unitTests.returnDefaultValues = true }。此外,你仍然可以将这些生命周期也抽离出来。例如如下: ```java @Override protected final void onResume() { super.onResume(); afterResume(); } protected void afterResume(){} ``` 108 | 现在,你可以在没有android设备的情况下,快速的测试了! 109 | ## 意外收获:使用adapter作为presenter 将Activity作为presente已经足够狡猾了吧?使用adapter作为presenter,你想过没有? 好吧,请看如下的代码: ```java public abstract class BasePresenterAdapter extends BaseAdapter { protected V vu; @Override public final View getView(int position, View convertView, ViewGroup parent) { if(convertView == null) { LayoutInflater inflater = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); try { vu = (V) getVuClass().newInstance(); vu.init(inflater, parent); convertView = vu.getView(); convertView.setTag(vu); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } else { vu = (V) convertView.getTag(); } if(convertView!=null) { onBindListItemVu(position); } return convertView; } protected abstract void onBindListItemVu(int position); protected abstract Class getVuClass(); } ``` 如上代码,使用adapter作为presenter其实和activity或者fragement几乎是一样的,只有一点明显的区别就是,我把onbingVu替换成了onBindListItemVu(接受int参数),其实我是借鉴了holderview模式 ## 总结和一个demo 我在这篇文章里介绍了我自己的一个实现MVP的方法。我非常期待其他android开发者对我的这套方法的反馈。我切实的想知道我的方法是否可行?我已经把这套思路用在一个框架的开发上,并且即将公布,与此同时,我在github上面有一个demo项目,感兴趣的人可以去看一下https://github.com/wongcain/MVP-Simple-Demo -------------------------------------------------------------------------------- /androidweekly/欢迎来到Android多进程时代/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/欢迎来到Android多进程时代/.DS_Store -------------------------------------------------------------------------------- /androidweekly/欢迎来到Android多进程时代/images/img01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/欢迎来到Android多进程时代/images/img01.gif -------------------------------------------------------------------------------- /androidweekly/欢迎来到Android多进程时代/images/img02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/欢迎来到Android多进程时代/images/img02.png -------------------------------------------------------------------------------- /androidweekly/欢迎来到Android多进程时代/images/img03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/欢迎来到Android多进程时代/images/img03.png -------------------------------------------------------------------------------- /androidweekly/欢迎来到Android多进程时代/images/img04.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/欢迎来到Android多进程时代/images/img04.gif -------------------------------------------------------------------------------- /androidweekly/欢迎来到Android多进程时代/readme.md: -------------------------------------------------------------------------------- 1 | 欢迎来到Android多进程时代 2 | --- 3 | 4 | > 5 | * 原文链接 : [Going multiprocess on Android](https://medium.com/@rotxed/going-multiprocess-on-android-52975ed8863c) 6 | * 译者 : [Lollypo](https://github.com/Lollypo) 7 | * 校对者: [Mr.Simple](https://github.com/bboyfeiyu) 8 | * 状态 : 完成 9 | 10 | **That moment when one Dalvik alone is no longer enough.** 11 | 12 | 13 | ## 生活在内存限制中 14 | 15 | 有很多方面使得Android成为一个独特的移动平台操作系统,但有时候却让人觉得难以融入,特别是从开发人员的角度看。 16 | 17 | 例如,把内存限制。iOS应用程序提供几乎没有限制的内存预算(200 MB不是什么大不了的事),Android有严重的局限性,从最近设备的24/32/48 MB以及旧设备极小的16 MB便可以看出。 18 | 19 | RAM预算就是一切你的应用运行时所能获得的全部了,这意味着,它必须满足加载类、线程、服务、资源和你的应用程序想要显示的内容。想象一个通过网格视图展示优美图片的照片浏览应用,或一个需要在后台播放的音乐播放器:这太恐怖了 20 | 21 | > 那时候你的体会应该是这样的 22 | 23 | ![Life’s a bitch, sometimes.](https://raw.githubusercontent.com/Lollypo/android-tech-frontier/master/others/%E6%AC%A2%E8%BF%8E%E6%9D%A5%E5%88%B0%E5%A4%9A%E7%BA%BF%E7%A8%8B%E6%97%B6%E4%BB%A3/images/img01.gif) 24 | 25 | 要理解为什么Android提出了这些限制以及提供了什么解决方案来应对他们,我们需要知道一点点在这背后之后发生了些什么。 26 | 27 | ## 理解Android进程 28 | 29 | 你应该已经知道了,安卓系统是基于Linux的。因此,每个应用程序都运行在其本身的进程(拥有一个独一无二的PID)中:这允许应用运行在一个相互隔离的环境中,不能被其他应用程序/进程干扰。通常来说,当你想启动一个应用程序,Android创建一个进程(从Zygote中fork出来的),并创建一个主线程,然后开始运行Main Activity。 30 | 31 | 你可能不知道的是,你可以指定应用程序的一些组件运行在不同的进程中,而不是那个被用于启动应用程序的。先来看一下这个Nice的属性: 32 |
33 | ```xml 34 | android:process 35 | ``` 36 |
37 | 38 | 该进程属性可用于activities、services、content providers和broadcast receivers 和指定的进程中应该执行的特定组件。 39 | 40 | 在这个例子中,我指定MusicService必须执行在一个单独的“music”的进程: 41 | ```xml 42 | 43 | 47 | 48 | 51 | 55 | 56 | 57 | ``` 58 | 59 | 它有什么意义呢? 60 | 61 | 在这个简短的介绍中,我提到了每一个Android应用程序在运行的时候都有一个不能超出的内存预算值。更精确的说,这限制了它只能在单个基础的进程上运行。换句话说,应用程序的每一个进程都将会有一个专门的内存预算(更不用说其中止时也有更酷的不同的规则) 62 | 63 | 让我们看看这种方法将是一件好事还是坏事。(剧透:两者都是) 64 | 65 | ## 使用多进程有啥好处 66 | 67 | 正如我刚才提到的,一个独立的进程可以充分利用自己的RAM预算,使其主进程拥有更多的空间处理资源。 68 | 69 | 此外,操作系统对待运行在不同组件中的进程是不一样的。这意味着,当系统运行在低可用内存的条件时,并不是所有的进程都会被杀死。想象一下:你的音乐播放器正在后台运行,音乐突然播放,系统需要释放一些内存(因为facebook,这就是原因)。由于播放音乐的服务跑在另一个进程中,一种极为可能的情况就是操作系统将会先杀死你的主进程(那个运行着你的UI的),而留下那个在另一个进程播放音乐的。 70 | 71 | 最后一点对于用户来说看起来似乎很不错!因为你的程序的每一个进程都有自身的在应用程序管理器上的屏幕显示RAM用度。其中有一个或多个将出现在“缓存”部分(这意味着它们是不活跃的)。 72 | 73 | > 正如你所看到的,Spotify在后台播放一些音乐。有一个活跃的带有服务的进程 [上图],而另一个进程(持有UI的)是缓存状态的,因为不再可见/不活动的[下图]。 74 | 75 | ![](https://raw.githubusercontent.com/Lollypo/android-tech-frontier/master/others/%E6%AC%A2%E8%BF%8E%E6%9D%A5%E5%88%B0%E5%A4%9A%E7%BA%BF%E7%A8%8B%E6%97%B6%E4%BB%A3/images/img02.png) 76 | ![](https://raw.githubusercontent.com/Lollypo/android-tech-frontier/master/others/%E6%AC%A2%E8%BF%8E%E6%9D%A5%E5%88%B0%E5%A4%9A%E7%BA%BF%E7%A8%8B%E6%97%B6%E4%BB%A3/images/img03.png) 77 | 78 | 79 | ## 使用多进程时的那些坑 80 | 81 | 不幸的是,坑有很多。事实上,你要学习拥有多个进程不是一下子就能完成的事 82 | 83 | 首先,进程是被设计成独立的(如安全特性),这意味着每一个进程将有自己的Dalvik VM实例。反过来,这意味着你不能通过这些实例共享数据,至少不是传统意义上的。例如,静态字段在每个进程都有自己的值,而不是你倾向于相信的只有一个值。并且这延伸到应用程序每一个的状态。 84 | 85 | 这是否意味着两个独立的进程之间互相交流是不可能的吗?不,实际上是可能的,有几种方法可以做到。最值得注意的是,Intent可以跨进程“旅行”,Handlers和Messengers也可以。。你也可以依靠AIDL(Android接口定义语言)和Binder,和你通常声明一个bound service茶不错(但你可以做更多的事!)。 86 | 87 | ## 我需要使用多进程吗 88 | 89 | 当然,这取决于你需要查看到的迹象。如果你的用户正在经历越来越频繁OutOfMemory错误或者他们抱怨你的应用程序是极其消耗RAM,你可能需要考虑使用一个或多个独立的进程。 90 | 91 | 音乐播放器的例子是第二个进程能使你的App做的更好的其中最常见的一个场景,当然,还有更多。 92 | 例如,你的应用程序是一个客户端云存储服务:委托同步服务组件专用的进程似乎是完全合理的,所以即使UI会被系统杀死,服务仍然可以运行并且保持文件更新。 93 | 94 | > 类似的情况会发生在你第一次真正意识到进程隔离的意思时 95 | 96 | ![This happens when you first realize what “isolation between processes” really means.](https://raw.githubusercontent.com/Lollypo/android-tech-frontier/master/others/%E6%AC%A2%E8%BF%8E%E6%9D%A5%E5%88%B0%E5%A4%9A%E7%BA%BF%E7%A8%8B%E6%97%B6%E4%BB%A3/images/img04.gif) 97 | 98 | 如果你认为你需要它,那么我建议你先玩一个小试验台应用:只有通过实际体验过使用多个进程的优势和其内在的复杂性,你才能够决定你是否真的需要它,如果是这样,什么是最好的处理它的方式而不至于把我们逼疯。 99 | 100 | ## 结语 101 | 102 | 我知道我仅仅触及到这个问题的表面,我只是想给你一些实用的建议,而不是告诉你在操作系统层调控进程的全部理论与工作机制。 103 | 104 | 还是那句话,如果你对此感兴趣并愿意深入其中,那就留言让我知道!同时,不要忘记文档是你最好的朋友[[1]](http://developer.android.com/guide/components/processes-and-threads.html#Processes) [[2]](https://developer.android.com/training/articles/memory.html) [[3]](https://developer.android.com/tools/debugging/debugging-memory.html) 105 | -------------------------------------------------------------------------------- /androidweekly/让你的Android应用能使用多种主题-Part-1/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/让你的Android应用能使用多种主题-Part-1/.DS_Store -------------------------------------------------------------------------------- /androidweekly/让你的Android应用能使用多种主题-Part-1/images/multiple-theme-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/让你的Android应用能使用多种主题-Part-1/images/multiple-theme-dark.png -------------------------------------------------------------------------------- /androidweekly/让你的Android应用能使用多种主题-Part-1/images/multiple-theme-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/让你的Android应用能使用多种主题-Part-1/images/multiple-theme-light.png -------------------------------------------------------------------------------- /androidweekly/让你的Android应用能使用多种主题-Part-1/readme.md: -------------------------------------------------------------------------------- 1 | 在你的Android App中支持多主题 2 | --- 3 | 4 | > 5 | * 原文链接 : [Supporting multiple themes in your Android app (Part 1)](http://www.hidroh.com/2015/02/16/support-multiple-themes-android-app/) 6 | * 译者 : [chaossss](https://github.com/chaossss) 7 | * 校对者: [Mr.Simple](https://github.com/bboyfeiyu) 8 | * 状态 : 完成 9 | 10 | 11 | ![](images/multiple-theme-dark.png) 12 | ![](images/multiple-theme-light.png) 13 | 14 | 我最近一直在忙着整我的黑客资讯App——Materialistic,今天难得有空,就让我来给大家分享一下我在Materialistic里使用的一个有趣的功能吧。 15 | 16 | 纵观现在的主流阅读类App,用户最常见的需求就是能够基于自己的阅读习惯选择明亮/灰暗两种风格的主题。为了用户的使用体验,我当然要为Materialistic添加这样的功能啦,要不然没人用我会很伤心的!而且很幸运的是,在Android里支持多种主题的切换并不麻烦(如果你的代码没有问题的话),实现这个功能蛮顺利的。所以今天我打算通过这篇博客给大家介绍我在Materialistic里面为了支持多种主题切换所使用的方法。 17 | 18 | 准备工作: 19 | 20 | 1. 你最少要有两个由Android 基本的light/dark主题衍生而来的主题。如果你使用了最新的appcompat-v7包,所对应的就是Theme.AppCompat.Light 或 Theme.AppCompat.Light.DarkActionBar(明亮风格),和Theme.AppCompat(灰暗风格)主题 21 | 22 | 1. 你需要为你的主题设置颜色。你可以在 [Google design spec](http://www.google.com/design/spec/style/color.html#color-color-palette " Google design spec website") 里面看到有关颜色搭配的指导 23 | 24 | 1. (可选项)为每一个主题的选项菜单图标加上颜色。取决于你的实现方式,染色过程可以是自动的,也可以是手动的,不过自动化的过程不就意味着你可以把一套图标应用于一种主题嘛,其他的调整只要改改颜色就可以了;但就Materialistic的实际需求来考虑,我还是为一个主题预留了多套不同的图标来避免麻烦…… 25 | 26 | 我今天就以明亮风格的主题来开始讲解吧: 27 | 28 | ## values/styles.xml ## 29 | 30 | 41 | 42 | ## values/colors.xml ## 43 | 44 | 45 | #FF9800 46 | #F57C00 47 | #FFE0B2 48 | 49 | #FF5252 50 | 51 | #FFFFFF 52 | #9E9E9E 53 | 54 | #DE000000 55 | #9E9E9E 56 | 57 | ## AndroidManifest.xml ## 58 | 59 | ... 60 | 61 | 62 | theme 中涉及的各种属性的含义可以在[Android Developers blog](http://android-developers.blogspot.sg/2014/10/appcompat-v21-material-design-for-pre.html "Android Developers blog") 里面找到解释 63 | 64 | ## 贴心小提示 ## 65 | 66 | > 虽然Android里面style的属性/值非常全面,我们想要实现的效果style基本上都包含了有,但是Android文档有关这些主题属性的解释特别少,尤其是对appcompat的解释。所以我们还是建议你写一个小Demo去测试style里面的属性/值应该怎么使用、能实现什么样的效果,然后再根据我们的需求去考虑使用哪些属性/值来实现我们想要的效果。 67 | 68 | 根据Android的Material Design规范,选项菜单图标的颜色应该和action bar上面的文字颜色保持一致,在我这是通过 android:textColorPrimary 来实现的,也就是使用#FFFFFF,基于这样的规范,我们需要为action bar提供一套白色的选项菜单图标。 69 | 70 | ## 贴心小提示 ## 71 | 72 | > Google 有在 [material-design-icons - Github](https://github.com/google/material-design-icons "Github") 上提供一些开源的Material Design图标哦。 73 | 74 | ## menu/my_menu.xml ## 75 | 76 | 77 | 79 | 81 | 83 | 84 | 85 | 为了使颜色一致,并且能让我们的Views和Texts能够在多个主题下被使用,最好的解决办法就是把颜色变成资源的引用,例如:android:textColor="@color/textColorPrimary;又或者是通过设置style来改变,例如:在textEmptyStyle.xml文件下,我们只使用被选中的颜色 86 | 87 | ## values/styles.xml ## 88 | 89 | 94 | 95 | 我相信通过今天在上面所介绍的这些内容已经足够让我们实现一个符合Material Design的明亮风格的主题了,下一篇博文我将会给大家介绍如何实现一个符合Material Design的灰暗风格的主题,以及如何在运行App的过程中切换主题。希望大家继续关注我的博客哦。 96 | -------------------------------------------------------------------------------- /androidweekly/让你的Android应用能使用多种主题-Part-2 + chaossss/template.md: -------------------------------------------------------------------------------- 1 | 让你的Android应用能使用多种主题 ( Part 2 ) 2 | --- 3 | 4 | > 5 | * 原文链接 : [Supporting multiple themes in your Android app (Part 2)](http://www.hidroh.com/2015/02/25/support-multiple-themes-android-app-part-2/) 6 | * 译者 : [chaossss](https://github.com/chaossss) 7 | * 校对者: [这里校对者的github用户名](github链接) 8 | * 状态 : 未完成 9 | 10 | 11 | 12 | 13 | `这里是翻译原文` 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /androidweekly/那些年我们错过的响应式编程/images/3324-amazed-face.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/那些年我们错过的响应式编程/images/3324-amazed-face.gif -------------------------------------------------------------------------------- /androidweekly/那些年我们错过的响应式编程/images/zclickstream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/那些年我们错过的响应式编程/images/zclickstream.png -------------------------------------------------------------------------------- /androidweekly/那些年我们错过的响应式编程/images/zmantra.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/那些年我们错过的响应式编程/images/zmantra.jpg -------------------------------------------------------------------------------- /androidweekly/那些年我们错过的响应式编程/images/zmulticlickstream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/那些年我们错过的响应式编程/images/zmulticlickstream.png -------------------------------------------------------------------------------- /androidweekly/那些年我们错过的响应式编程/images/zresponsemetastream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/那些年我们错过的响应式编程/images/zresponsemetastream.png -------------------------------------------------------------------------------- /androidweekly/那些年我们错过的响应式编程/images/zresponsestream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/那些年我们错过的响应式编程/images/zresponsestream.png -------------------------------------------------------------------------------- /androidweekly/那些年我们错过的响应式编程/images/ztwitterbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/androidweekly/那些年我们错过的响应式编程/images/ztwitterbox.png -------------------------------------------------------------------------------- /androidweekly/那些年我们错过的响应式编程/readme.md: -------------------------------------------------------------------------------- 1 | ## 那些年我们错过的响应式编程 2 | (by [@andrestaltz](https://twitter.com/andrestaltz)) 3 | --- 4 | 5 | > 6 | * 原文链接 : [The introduction to Reactive Programming you've been missing](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) 7 | * 译者 : [yaoqinwei](https://github.com/yaoqinwei) 8 | * 校对者: [yaoqinwei](https://github.com/yaoqinwei)、[bboyfeiyu](https://github.com/bboyfeiyu)、[chaossss](https://github.com/chaossss) 9 | * 状态 : 未完成 10 | 11 | >译者注:校对时请注意几个核心词翻译是否准确,stream、event stream、date stream,文中交替的出现,可能会把人搞糊涂,不造怎么翻译才好 12 | 13 | So you're curious in learning this new thing called Reactive Programming, particularly its variant comprising of Rx, Bacon.js, RAC, and others. 14 | 15 | 相信你们在学习响应式编程这个新技术的时候都会充满了好奇,特别是它的一些变体,包括Rx系列、Bacon.js、RAC和其他的一些变体。 16 | 17 | Learning it is hard, even harder by the lack of good material. When I started, I tried looking for tutorials. I found only a handful of practical guides, but they just scratched the surface and never tackled the challenge of building the whole architecture around it. Library documentations often don't help when you're trying to understand some function. I mean, honestly, look at this: 18 | 19 | > **Rx.Observable.prototype.flatMapLatest(selector, [thisArg])** 20 | 21 | > Projects each element of an observable sequence into a new sequence of observable sequences by incorporating the element's index and then transforms an observable sequence of observable sequences into an observable sequence producing values only from the most recent observable sequence. 22 | 23 | Holy cow. 24 | 25 | 学习响应式编程是个困难的过程,尤其是在当前缺乏优秀资料的前提下。起初,我试图寻找一些教程,却只找到了少量的实践指南,而且它们讲的都非常浅显(scratched the surface),从来没人接受围绕着响应式编程建立一个完整知识体系的挑战。而官方文档通常也并不能完全地帮助你理解某些函数,它们通常看起来很绕,不信请看这里: 26 | 27 | > **Rx.Observable.prototype.flatMapLatest(selector, [thisArg])** 28 | 29 | > 根据元素下标,将可观察序列中每个元素一一映射到一个新的可观察序列当中,然后...%…………%&¥#@@……&**(晕了) 30 | 31 | 天呐,这简直太绕了! 32 | 33 | I've read two books, one just painted the big picture, while the other dived into how to use the Reactive library. I ended up learning Reactive Programming the hard way: figuring it out while building with it. At my work in Futurice I got to use it in a real project, and had the support of some colleagues when I ran into troubles. 34 | 35 | The hardest part of the learning journey is thinking in Reactive. It's a lot about letting go of old imperative and stateful habits of typical programming, and forcing your brain to work in a different paradigm. I haven't found any guide on the internet in this aspect, and I think the world deserves a practical tutorial on how to think in Reactive, so that you can get started. Library documentation can light your way after that. I hope this helps you. 36 | 37 | 我读过两本相关的书,一本只是在给你描绘响应式编程的伟大景象,而另一本却只是深入到如何使用响应式库而已。我在不断的构建(building)中把响应式编程了解的透彻了一些,最后以这种艰难的方式学完了响应式编程。在我工作公司的一个真实项目中我会用到它,当我遇到问题时,还可以得到同事的支持。 38 | 39 | 学习过程中最难的部分是**如何以响应式的方式来思考**,更多的意味着要摒弃那些老旧的命令式和状态式的典型编程习惯,并且强迫自己的大脑以不同的范式来运作。我还没有在网络上找到任何一个教程是从这个层面来剖析的,我觉得这个世界非常值得拥有一个优秀的实践教程来教你**如何以响应式编程的方式来思考**,方便引导你开始学习响应式编程。然后看各种库文档才可以给你更多的指引。希望这篇文章能够帮助你快速地进入**响应式编程**的世界。 40 | 41 | ## "What is Reactive Programming?" 42 | 43 | There are plenty of bad explanations and definitions out there on the internet. [Wikipedia](https://en.wikipedia.org/wiki/Reactive_programming) is too generic and theoretical as usual. [Stackoverflow](http://stackoverflow.com/questions/1028250/what-is-functional-reactive-programming)'s canonical answer is obviously not suitable for newcomers. [Reactive Manifesto](http://www.reactivemanifesto.org/) sounds like the kind of thing you show to your project manager or the businessmen at your company. Microsoft's [Rx terminology](https://rx.codeplex.com/) "Rx = Observables + LINQ + Schedulers" is so heavy and Microsoftish that most of us are left confused. Terms like "reactive" and "propagation of change" don't convey anything specifically different to what your typical MV* and favorite language already does. Of course my framework views react to the models. Of course change is propagated. If it wouldn't, nothing would be rendered. 44 | 45 | So let's cut the bullshit. 46 | 47 | ## "什是响应式编程?" 48 | 49 | 网络上有一大堆糟糕的解释和定义,[Wikipedia](https://en.wikipedia.org/wiki/Reactive_programming)上通常都是些非常笼统和理论性的解释,[Stackoverflow](http://stackoverflow.com/questions/1028250/what-is-functional-reactive-programming)上的一些规范的回答显然也不适合新手来参考,[Reactive Manifesto](http://www.reactivemanifesto.org/)看起来也只像是拿给你的PM或者老板看的东西,微软的[Rx术语](https://rx.codeplex.com/)"Rx = Observables + LINQ + Schedulers" 也显得太过沉重,而且充满了太多微软式的东西,反而给我们留下了更多的疑惑。相对于你使用的MV*框架以及你钟爱的编程语言,"Reactive"和"Propagation of change"这样的术语并没有传达任何有意义的概念。当然,我的view框架能够从model做出反应,我的改变当然也会传播,如果没有这些,我的界面根本就没有东西可渲染(译者:最后一句旨在说明微软的术语化的东西都非常老套、非常教科书化)。 50 | 51 | 所以,不要再扯这些废话了。 52 | 53 | #### Reactive programming is programming with asynchronous data streams. 54 | 55 | In a way, this isn't anything new. Event buses or your typical click events are really an asynchronous event stream, on which you can observe and do some side effects. Reactive is that idea on steroids. You are able to create data streams of anything, not just from click and hover events. Streams are cheap and ubiquitous, anything can be a stream: variables, user inputs, properties, caches, data structures, etc. For example, imagine your Twitter feed would be a data stream in the same fashion that click events are. You can listen to that stream and react accordingly. 56 | 57 | #### 响应式编程就是与异步数据流交互的编程范式 58 | 59 | 一方面,这已经不是什么新事物了。事件总线(Event Buses)或一些典型的点击事件本质上就是一个异步事件流(asynchronous event stream),这样你就可以观察它的变化并使其做出一些反应(产生一些效果(do some side effects))。响应式是这样的一个思路:除了点击(click)和悬停(hover)的事件外,你可以给任何事物创建数据流。数据流无处不在(Streams are cheap and ubiquitous),任何东西都可以成为一个数据流,例如变量、用户输入、属性、缓存、数据结构等等。举个栗子,你可以把你的微博订阅功能想象成跟点击事件一样的数据流,你可以监听这样的数据流,并做出相应的反应。 60 | 61 | **On top of that, you are given an amazing toolbox of functions to combine, create and filter any of those streams.** That's where the "functional" magic kicks in. A stream can be used as an input to another one. Even multiple streams can be used as inputs to another stream. You can _merge_ two streams. You can _filter_ a stream to get another one that has only those events you are interested in. You can _map_ data values from one stream to another new one. 62 | 63 | If streams are so central to Reactive, let's take a careful look at them, starting with our familiar "clicks on a button" event stream. 64 | 65 | ![Click event stream](images/zclickstream.png) 66 | 67 | A stream is a sequence of **ongoing events ordered in time**. It can emit three different things: a value (of some type), an error, or a "completed" signal. Consider that the "completed" takes place, for instance, when the current window or view containing that button is closed. 68 | 69 | We capture these emitted events only **asynchronously**, by defining a function that will execute when a value is emitted, another function when an error is emitted, and another function when 'completed' is emitted. Sometimes these last two can be omitted and you can just focus on defining the function for values. The "listening" to the stream is called **subscribing**. The functions we are defining are observers. The stream is the subject (or "observable") being observed. This is precisely the [Observer Design Pattern](https://en.wikipedia.org/wiki/Observer_pattern). 70 | 71 | An alternative way of drawing that diagram is with ASCII, which we will use in some parts of this tutorial: 72 | ``` 73 | --a---b-c---d---X---|-> 74 | 75 | a, b, c, d are emitted values 76 | X is an error 77 | | is the 'completed' signal 78 | ---> is the timeline 79 | ``` 80 | Since this feels so familiar already, and I don't want you to get bored, let's do something new: we are going to create new click event streams transformed out of the original click event stream. 81 | 82 | **最重要的是,你会拥有一些令人惊艳的函数去结合(combine)、创建(create)和过滤(filter)任何一组数据流。** 这就是"函数式编程"的魔力所在。一个**数据流**可以作为另一个**数据流**的输入,甚至多个**数据流**也可以作为另一个**数据流**的输入。你可以_合并(merge)_两个**数据流**,也可以_过滤(filter)_一个数据流得到另一个只包含你感兴趣的事件的**数据流**,还可以_映射(map)_一个**数据流**的值到一个新的**数据流**里。 83 | 84 | 如果**数据流**对于响应式是如此的核心(so central to Reactive),那就让我们来仔细的看看它们,先从我们熟悉的"点击一个按钮"的**事件流**开始 85 | 86 | ![Click event stream](images/zclickstream.png) 87 | 88 | 一个**数据流**是一个**按时间排序的即将发生的事件(Ongoing events ordered in time)**的序列。如上图,它可以发出3种不同的事件(上一句已经把它们叫做事件):一个某种类型的**值事件**,一个**错误事件**和一个**完成事件**。当一个**完成事件**发生时,在某些情况下,我们可能会做这样的操作:关闭包含那个按钮的窗口或者视图组件。 89 | 90 | 我们只能**异步的**的去捕捉这些被发出的事件,这样我们就可以在发出一个**值事件**时执行一个函数,发出**错误事件**时执行一个函数,发出**完成事件**时执行另一个函数。有时候你可以忽略后两个事件,只需聚焦于如何定义和设计在发出**值事件**时要执行的函数,监听这个**事件流**的过程叫做**订阅**,我们定义的函数叫做**观察者**,而事件流就可以叫做被观察的**主题**(或者叫被观察者)。你应该察觉到了,对的,它就是[**观察者模式**](https://en.wikipedia.org/wiki/Observer_pattern)。 91 | 92 | 上面的示意图我们也可以用ASCII码的形式重新画一遍,请注意,下面的部分教程中我们会继续使用这幅图: 93 | ``` 94 | --a---b-c---d---X---|-> 95 | 96 | a, b, c, d 是值事件 97 | X 是错误事件 98 | | 是完成事件 99 | ---> 是时间线(轴) 100 | ``` 101 | 102 | 现在你对响应式编程事件流应该非常熟悉了,为了不让你感到无聊,让我们来做一些新的尝试吧:我们将创建一个由原始点击事件流演变而来的一种新的点击事件流。 103 | 104 | First, let's make a counter stream that indicates how many times a button was clicked. In common Reactive libraries, each stream has many functions attached to it, such as `map`, `filter`, `scan`, etc. When you call one of these functions, such as `clickStream.map(f)`, it returns a **new stream** based on the click stream. It does not modify the original click stream in any way. This is a property called **immutability**, and it goes together with Reactive streams just like pancakes are good with syrup. That allows us to chain functions like `clickStream.map(f).scan(g)`: 105 | 106 | ``` 107 | clickStream: ---c----c--c----c------c--> 108 | vvvvv map(c becomes 1) vvvv 109 | ---1----1--1----1------1--> 110 | vvvvvvvvv scan(+) vvvvvvvvv 111 | counterStream: ---1----2--3----4------5--> 112 | ``` 113 | 114 | The `map(f)` function replaces (into the new stream) each emitted value according to a function `f` you provide. In our case, we mapped to the number 1 on each click. The `scan(g)` function aggregates all previous values on the stream, producing value `x = g(accumulated, current)`, where `g` was simply the add function in this example. Then, `counterStream` emits the total number of clicks whenever a click happens. 115 | 116 | 首先,让我们来创建一个记录按钮点击次数的事件流。在常用的响应式库中,每个事件流都会附有一些函数,例如 `map`, `filter`, `scan`等,当你调用这其中的一个方法时,比如`clickStream.map(f)`,它会返回基于点击事件流的一个**新事件流**。它不会对原来的点击事件流做任何的修改。这种特性叫做**不可变性(immutability)**,而且它可以和响应式事件流搭配在一起使用,就像豆浆和油条一样完美的搭配。这样我们可以用链式函数的方式来调用,例如:`clickStream.map(f).scan(g)`: 117 | 118 | ``` 119 | clickStream: ---c----c--c----c------c--> 120 | vvvvv map(c becomes 1) vvvv 121 | ---1----1--1----1------1--> 122 | vvvvvvvvv scan(+) vvvvvvvvv 123 | counterStream: ---1----2--3----4------5--> 124 | ``` 125 | 126 | `map(f)`函数会根据你提供的`f`函数把原事件流中每一个返回值分别映射到新的事件流中。在上图的例子中,我们把每一次点击事件都映射成数字1,`scan(g)`函数则把之前映射的值聚集起来,然后根据`x = g(accumulated, current)`算法来作相应的处理,而本例的`g`函数其实就是简单的加法函数。然后,当一个点击事件发生时,`counterStream`函数则上报当前点击事件总数。 127 | 128 | To show the real power of Reactive, let's just say that you want to have a stream of "double click" events. To make it even more interesting, let's say we want the new stream to consider triple clicks as double clicks, or in general, multiple clicks (two or more). Take a deep breath and imagine how you would do that in a traditional imperative and stateful fashion. I bet it sounds fairly nasty and involves some variables to keep state and some fiddling with time intervals. 129 | 130 | Well, in Reactive it's pretty simple. In fact, the logic is just [4 lines of code](http://jsfiddle.net/staltz/4gGgs/27/). 131 | But let's ignore code for now. Thinking in diagrams is the best way to understand and build streams, whether you're a beginner or an expert. 132 | 133 | ![Multiple clicks stream](images/zmulticlickstream.png) 134 | 135 | Grey boxes are functions transforming one stream into another. First we accumulate clicks in lists, whenever 250 milliseconds of "event silence" has happened (that's what `buffer(stream.throttle(250ms))` does, in a nutshell. Don't worry about understanding the details at this point, we are just demoing Reactive for now). The result is a stream of lists, from which we apply `map()` to map each list to an integer matching the length of that list. Finally, we ignore `1` integers using the `filter(x >= 2)` function. That's it: 3 operations to produce our intended stream. We can then subscribe ("listen") to it to react accordingly how we wish. 136 | 137 | I hope you enjoy the beauty of this approach. This example is just the tip of the iceberg: you can apply the same operations on different kinds of streams, for instance, on a stream of API responses; on the other hand, there are many other functions available. 138 | 139 | 为了展示响应式编程真正的魅力,我们假设你有一个"双击"事件流,为了让它更有趣,我们假设这个事件流同时处理"三次点击"或者"多次点击"事件,然后深吸一口气想想如何用传统的命令式和状态式的方式来处理,我敢打赌,这么做会相当的讨厌,其中还要涉及到一些变量来保存状态,并且还得做一些时间间隔的调整。 140 | 141 | 而用响应式编程的方式处理会非常的简洁,实际上,逻辑处理部分只需要[四行代码](http://jsfiddle.net/staltz/4gGgs/27/)。但是,当前阶段让我们现忽略代码的部分,无论你是新手还是专家,看着图表思考来理解和建立事件流将是一个非常棒的方法。 142 | 143 | ![多次点击事件流](images/zmulticlickstream.png) 144 | 145 | 图中,灰色盒子表示将上面的事件流转换下面的事件流的**函数**过程,首先根据250毫秒的间隔时间(event silence, 译者:无事件发生的时间段,上一个事件发生到下一个事件发生的间隔时间)把点击事件流一段一隔开,再将每一段的一个或多个点击事件添加到列表中(这就是这个函数:`buffer(stream.throttle(250ms))`所做的事情,当前我们先不要急着去理解细节,我们只需专注响应式的部分先)。现在我们得到的是多个含有事件流的列表,然后我们使用了`map()`中的函数来算出每一个列表长度的整数数值映射到下一个事件流当中。最后我们使用了过滤`filter(x >= 2)` 函数忽略掉了小于`1` 的整数。就这样,我们用了3步操作生成了我们想要的事件流,接下来,我们就可以订阅("监听")这个事件并作出我们想要的操作了。 146 | 147 | 我希望你能感受到这个示例的优雅之处。当然了,这个示例也只是响应式编程魔力的冰山一角而已,你同样可以将这3步操作应用到不同种类的事件流中去,例如,一串API响应的事件流。另一方面,你还有非常多的函数可以使用。 148 | 149 | ## "Why should I consider adopting RP?" 150 | 151 | Reactive Programming raises the level of abstraction of your code so you can focus on the interdependence of events that define the business logic, rather than having to constantly fiddle with a large amount of implementation details. Code in RP will likely be more concise. 152 | 153 | The benefit is more evident in modern webapps and mobile apps that are highly interactive with a multitude of UI events related to data events. 10 years ago, interaction with web pages was basically about submitting a long form to the backend and performing simple rendering to the frontend. Apps have evolved to be more real-time: modifying a single form field can automatically trigger a save to the backend, "likes" to some content can be reflected in real time to other connected users, and so forth. 154 | 155 | Apps nowadays have an abundancy of real-time events of every kind that enable a highly interactive experience to the user. We need tools for properly dealing with that, and Reactive Programming is an answer. 156 | 157 | ## "我为什么要采用响应式编程?" 158 | 159 | 响应式编程可以提高你的代码抽象级别,好让你可以专注于定义与事件相互依存的业务逻辑,而不是把大量精力放在实现细节上,使用响应式编程会让你的代码变得更加简洁。 160 | 161 | 特别对于现在流行的webapps和mobile apps,这些频繁与数据事件相关的大量UI事件交互的程序,好处就更加的明显了。十年前,web页面的交互是通过提交一个很长的表单数据到后端,然后再做一些简单的前端渲染操作。而现在的Apps则演变的更具有实时性:仅仅修改一个单独的表单域就能自动的触发保存到后端的代码,就像某个用户对一些内容点了赞,就能够实时反映到其他已连接的用户一样,等等。 162 | 163 | 当今的Apps都含有丰富的实时事件来保证一个高效的用户体验,我们就需要采用一个合适的工具来处理,那么响应式编程就正好是我们想要的答案。 164 | 165 | ## Thinking in RP, with examples 166 | 167 | Let's dive into the real stuff. A real-world example with a step-by-step guide on how to think in RP. No synthetic examples, no half-explained concepts. By the end of this tutorial we will have produced real functioning code, while knowing why we did each thing. 168 | 169 | I picked **JavaScript** and **[RxJS](https://github.com/Reactive-Extensions/RxJS)** as the tools for this, for a reason: JavaScript is the most familiar language out there at the moment, and the [Rx* library family](http://www.reactivex.io) is widely available for many languages and platforms ([.NET](https://rx.codeplex.com/), [Java](https://github.com/Netflix/RxJava), [Scala](https://github.com/Netflix/RxJava/tree/master/language-adaptors/rxjava-scala), [Clojure](https://github.com/Netflix/RxJava/tree/master/language-adaptors/rxjava-clojure), [JavaScript](https://github.com/Reactive-Extensions/RxJS), [Ruby](https://github.com/Reactive-Extensions/Rx.rb), [Python](https://github.com/Reactive-Extensions/RxPy), [C++](https://github.com/Reactive-Extensions/RxCpp), [Objective-C/Cocoa](https://github.com/ReactiveCocoa/ReactiveCocoa), [Groovy](https://github.com/Netflix/RxJava/tree/master/language-adaptors/rxjava-groovy), etc). So whatever your tools are, you can concretely benefit by following this tutorial. 170 | 171 | ## 以响应式编程方式思考的例子 172 | 173 | 让我们深入到一些真实的例子,一个能够一步一步教你如何以响应式编程的方式思考的例子,没有虚构的示例,没有一知半解的概念。在这个教程的末尾我们将产生一些真实的函数代码,并能够知晓每一步为什么那样做的原因(知其然,知其所以然)。 174 | 175 | 我选了**JavaScript**和**[RxJS](https://github.com/Reactive-Extensions/RxJS)**来作为本教程的编程语言,原因是:JavaScript是目前最多人熟悉的语言,而[Rx系列的库](http://www.reactivex.io)对于很多语言和平台的运用是非常广泛的,例如([.NET](https://rx.codeplex.com/), [Java](https://github.com/Netflix/RxJava), [Scala](https://github.com/Netflix/RxJava/tree/master/language-adaptors/rxjava-scala), [Clojure](https://github.com/Netflix/RxJava/tree/master/language-adaptors/rxjava-clojure), [JavaScript](https://github.com/Reactive-Extensions/RxJS), [Ruby](https://github.com/Reactive-Extensions/Rx.rb), [Python](https://github.com/Reactive-Extensions/RxPy), [C++](https://github.com/Reactive-Extensions/RxCpp), [Objective-C/Cocoa](https://github.com/ReactiveCocoa/ReactiveCocoa), [Groovy](https://github.com/Netflix/RxJava/tree/master/language-adaptors/rxjava-groovy)等等。所以,无论你用的是什么语言、库、工具,你都能从下面这个教程中学到东西(从中受益)。 176 | 177 | ## Implementing a "Who to follow" suggestions box 178 | 179 | In Twitter there is this UI element that suggests other accounts you could follow: 180 | 181 | ![Twitter Who to follow suggestions box](images/ztwitterbox.png) 182 | 183 | We are going to focus on imitating its core features, which are: 184 | 185 | * On startup, load accounts data from the API and display 3 suggestions 186 | * On clicking "Refresh", load 3 other account suggestions into the 3 rows 187 | * On click 'x' button on an account row, clear only that current account and display another 188 | * Each row displays the account's avatar and links to their page 189 | 190 | We can leave out the other features and buttons because they are minor. And, instead of Twitter, which recently closed its API to the unauthorized public, let's build that UI for following people on Github. There's a [Github API for getting users](https://developer.github.com/v3/users/#get-all-users). 191 | 192 | The complete code for this is ready at http://jsfiddle.net/staltz/8jFJH/48/ in case you want to take a peak already. 193 | 194 | ## 实现一个推荐关注(Who to follow)的功能 195 | 196 | 在Twitter里有一个UI元素向你推荐你可以关注的用户,如下图: 197 | 198 | ![Twitter Who to follow suggestions box](images/ztwitterbox.png) 199 | 200 | 我们将聚焦于模仿它的主要功能,它们是: 201 | 202 | * 开始阶段,从API加载推荐关注的用户账户数据,然后显示三个推荐用户 203 | * 点击刷新,加载另外三个推荐用户到当前的三行中显示 204 | * 点击每一行的推荐用户上的'x'按钮,清楚当前被点击的用户,并显示新的一个用户到当前行 205 | * 每一行显示一个用户的头像并且在点击之后可以链接到他们的主页。 206 | 207 | 我们可以先不管其他的功能和按钮,因为它们是次要的。因为Twitter最近关闭了未经授权的公共API调用,我们将用[Github获取用户的API](https://developer.github.com/v3/users/#get-all-users)代替,并且以此来构建我们的UI。 208 | 209 | 如果你想先看一下最终效果,这里有完成后的[代码](http://jsfiddle.net/staltz/8jFJH/48/)。 210 | 211 | ## Request and response 212 | 213 | **How do you approach this problem with Rx?** Well, to start with, (almost) _everything can be a stream_. That's the Rx mantra. Let's start with the easiest feature: "on startup, load 3 accounts data from the API". There is nothing special here, this is simply about (1) doing a request, (2) getting a response, (3) rendering the response. So let's go ahead and represent our requests as a stream. At first this will feel like overkill, but we need to start from the basics, right? 214 | 215 | On startup we need to do only one request, so if we model it as a data stream, it will be a stream with only one emitted value. Later, we know we will have many requests happening, but for now, it is just one. 216 | 217 | ``` 218 | --a------|-> 219 | 220 | Where a is the string 'https://api.github.com/users' 221 | ``` 222 | 223 | This is a stream of URLs that we want to request. Whenever a request event happens, it tells us two things: when and what. "When" the request should be executed is when the event is emitted. And "what" should be requested is the value emitted: a string containing the URL. 224 | 225 | To create such stream with a single value is very simple in Rx*. The official terminology for a stream is "Observable", for the fact that it can be observed, but I find it to be a silly name, so I call it _stream_. 226 | 227 | ```javascript 228 | var requestStream = Rx.Observable.just('https://api.github.com/users'); 229 | ``` 230 | 231 | But now, that is just a stream of strings, doing no other operation, so we need to somehow make something happen when that value is emitted. That's done by [subscribing](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypesubscribeobserver--onnext-onerror-oncompleted) to the stream. 232 | 233 | ```javascript 234 | requestStream.subscribe(function(requestUrl) { 235 | // execute the request 236 | jQuery.getJSON(requestUrl, function(responseData) { 237 | // ... 238 | }); 239 | } 240 | ``` 241 | 242 | ## Request和Response 243 | 244 | **在Rx中是怎么处理这个问题呢?**,在开始之前,我们要明白,(几乎)_一切都可以成为一个事件流_,这就是Rx的准则(mantra)。让我们从最简单的功能开始:"开始阶段,从API加载推荐关注的用户账户数据,然后显示三个推荐用户"。其实这个功能没什么特殊的,简单的步骤分为: (1)发出一个请求,(2)获取响应数据,(3)渲染响应数据。ok,让我们把请求作为一个事件流,一开始你可能会觉得这样做有些夸张,但别急,我们也得从最基本的开始,不是吗? 245 | 246 | 开始时我们只需做一次请求,如果我们把它作为一个数据流的话,它只能成为一个仅仅返回一个值的事件流而已。一会儿我们还会有很多请求要做,但当前,只有一个。 247 | 248 | ``` 249 | --a------|-> 250 | 251 | a就是字符串:'https://api.github.com/users' 252 | ``` 253 | 254 | 这是一个我们要请求的URL事件流。每当发生一个请求时,它将告诉我们两件事:**什么时候**和**做了什么事**(when and what)。**什么时候**请求被执行,什么时候事件就被发出。而**做了什么**就是请求了什么,也就是请求的URL字符串。 255 | 256 | 在Rx中,创建返回一个值的事件流是非常简单的。其实事件流在Rx里的术语是叫"被观察者",也就是说它是可以被观察的,但是我发现这名字比较傻,所以我更喜欢把它叫做_事件流_。 257 | 258 | ```javascript 259 | var requestStream = Rx.Observable.just('https://api.github.com/users'); 260 | ``` 261 | 262 | 但现在,这只是一个字符串的事件流而已,并没有做其他操作,所以我们需要在发出这个值的时候做一些我们要做的操作,可以通过[订阅(subscribing)](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypesubscribeobserver--onnext-onerror-oncompleted)这个事件来实现。 263 | 264 | ```javascript 265 | requestStream.subscribe(function(requestUrl) { 266 | // execute the request 267 | jQuery.getJSON(requestUrl, function(responseData) { 268 | // ... 269 | }); 270 | } 271 | ``` 272 | 273 | Notice we are using a jQuery Ajax callback (which we assume you [should know already](http://devdocs.io/jquery/jquery.getjson)) to handle the asynchronicity of the request operation. But wait a moment, Rx is for dealing with **asynchronous** data streams. Couldn't the response for that request be a stream containing the data arriving at some time in the future? Well, at a conceptual level, it sure looks like it, so let's try that. 274 | 275 | ```javascript 276 | requestStream.subscribe(function(requestUrl) { 277 | // execute the request 278 | var responseStream = Rx.Observable.create(function (observer) { 279 | jQuery.getJSON(requestUrl) 280 | .done(function(response) { observer.onNext(response); }) 281 | .fail(function(jqXHR, status, error) { observer.onError(error); }) 282 | .always(function() { observer.onCompleted(); }); 283 | }); 284 | 285 | responseStream.subscribe(function(response) { 286 | // do something with the response 287 | }); 288 | } 289 | ``` 290 | 291 | What [`Rx.Observable.create()`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservablecreatesubscribe) does is create your own custom stream by explicitly informing each observer (or in other words, a "subscriber") about data events (`onNext()`) or errors (`onError()`). What we did was just wrap that jQuery Ajax Promise. **Excuse me, does this mean that a Promise is an Observable?** 292 | 293 |   294 |   295 |   296 |   297 |   298 | 299 | ![Amazed](images/3324-amazed-face.gif) 300 | 301 | 注意到我们这里使用的是JQuery的AJAX回调方法(我们假设你[已经很了解JQuery和AJAX了](http://devdocs.io/jquery/jquery.getjson))来的处理这个异步的请求操作。但是,请稍等一下,Rx就是用来处理**异步数据流**的,难道它就不能处理来自请求(request)在未来某个时间响应(response)的数据流吗?好吧,理论上是可以的,让我们尝试一下。 302 | 303 | ```javascript 304 | requestStream.subscribe(function(requestUrl) { 305 | // execute the request 306 | var responseStream = Rx.Observable.create(function (observer) { 307 | jQuery.getJSON(requestUrl) 308 | .done(function(response) { observer.onNext(response); }) 309 | .fail(function(jqXHR, status, error) { observer.onError(error); }) 310 | .always(function() { observer.onCompleted(); }); 311 | }); 312 | 313 | responseStream.subscribe(function(response) { 314 | // do something with the response 315 | }); 316 | } 317 | ``` 318 | 319 | [`Rx.Observable.create()`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservablecreatesubscribe)操作就是在创建自己定制的事件流,且对于数据事件(`onNext()`)和错误事件(`onError()`)都会显示的通知该事件每一个观察者(或订阅者)。我们做的只是小小的封装一下jQuery Ajax Promise而已。**等等,这是否意味者jQuery Ajax Promise本质上就是一个被观察者呢(Observable)?** 320 | 321 |   322 |   323 |   324 |   325 |   326 | 327 | ![Amazed](images/3324-amazed-face.gif) 328 | 329 | Yes. 330 | 331 | Observable is Promise++. In Rx you can easily convert a Promise to an Observable by doing `var stream = Rx.Observable.fromPromise(promise)`, so let's use that. The only difference is that Observables are not [Promises/A+](http://promises-aplus.github.io/promises-spec/) compliant, but conceptually there is no clash. A Promise is simply an Observable with one single emitted value. Rx streams go beyond promises by allowing many returned values. 332 | 333 | This is pretty nice, and shows how Observables are at least as powerful as Promises. So if you believe the Promises hype, keep an eye on what Rx Observables are capable of. 334 | 335 | Now back to our example, if you were quick to notice, we have one `subscribe()` call inside another, which is somewhat akin to callback hell. Also, the creation of `responseStream` is dependent on `requestStream`. As you heard before, in Rx there are simple mechanisms for transforming and creating new streams out of others, so we should be doing that. 336 | 337 | The one basic function that you should know by now is [`map(f)`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypemapselector-thisarg), which takes each value of stream A, applies `f()` on it, and produces a value on stream B. If we do that to our request and response streams, we can map request URLs to response Promises (disguised as streams). 338 | 339 | ```javascript 340 | var responseMetastream = requestStream 341 | .map(function(requestUrl) { 342 | return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); 343 | }); 344 | ``` 345 | 346 | Then we will have created a beast called "_metastream_": a stream of streams. Don't panic yet. A metastream is a stream where each emitted value is yet another stream. You can think of it as [pointers](https://en.wikipedia.org/wiki/Pointer_(computer_programming)): each emitted value is a _pointer_ to another stream. In our example, each request URL is mapped to a pointer to the promise stream containing the corresponding response. 347 | 348 | ![Response metastream](images/zresponsemetastream.png) 349 | 350 | 是的。 351 | 352 | Promise++就是被观察者(Observable),在Rx里你可以使用这样的操作:`var stream = Rx.Observable.fromPromise(promise)`,就可以很轻松的将Promise转换成一个被观察者(Observable),如此方便,让我们现在就开始使用它吧。不同的是,这些被观察者都不能兼容[Promises/A+](http://promises-aplus.github.io/promises-spec/),但理论上并不冲突。一个Promise就是一个只有一个返回值的简单的被观察者,而Rx就远超于Promise,它允许多个值返回。 353 | 354 | 这样更好,这样更突出被观察者至少比Promise强大,所以如果你相信Promise宣传的东西,那么也请留意一下响应式编程能胜任些什么。 355 | 356 | 现在回到示例当中,你应该能快速发现,我们在`subscribe()`方法的内部再次调用了`subscribe()`方法,这有点类似于回调地狱(callback hell),而且`responseStream`的创建也是依赖于`requestStream`的。在之前我们说过,在Rx里,有很多很简单的机制来从其他事件流的转化并创建出一些新的事件流,那么,我们也应该这样做试试。 357 | 358 | 现在你需要了解的一个最基本的函数是[`map(f)`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypemapselector-thisarg),它可以从事件流A中取出每一个值,并对每一个值执行`f()`函数,然后将产生的新值填充到事件流B。如果将它应用到我们的请求和响应事件流当中,那我们就可以将请求的URL映射到一个响应Promises上了(伪装成数据流)。 359 | 360 | ```javascript 361 | var responseMetastream = requestStream 362 | .map(function(requestUrl) { 363 | return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); 364 | }); 365 | ``` 366 | 367 | 然后,我们创造了一个叫做"_metastream_"的怪兽:一个装载了事件流的事件流。先别惊慌,metastream就是每一个发出的值都是另一个事件流的事件流,你看把它想象成一个[指针(pointers)]((https://en.wikipedia.org/wiki/Pointer_(computer_programming))数组:每一个单独发出的值就是一个_指针_,它指向另一个事件流。在我们的示例里,每一个请求URL都映射到一个指向包含响应数据的promise数据流。 368 | 369 | ![Response metastream](images/zresponsemetastream.png) 370 | 371 | A metastream for responses looks confusing, and doesn't seem to help us at all. We just want a simple stream of responses, where each emitted value is a JSON object, not a 'Promise' of a JSON object. Say hi to [Mr. Flatmap](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypeflatmapselector-resultselector): a version of `map()` than "flattens" a metastream, by emitting on the "trunk" stream everything that will be emitted on "branch" streams. Flatmap is not a "fix" and metastreams are not a bug, these are really the tools for dealing with asynchronous responses in Rx. 372 | 373 | ```javascript 374 | var responseStream = requestStream 375 | .flatMap(function(requestUrl) { 376 | return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); 377 | }); 378 | ``` 379 | 380 | ![Response stream](images/zresponsestream.png) 381 | 382 | Nice. And because the response stream is defined according to request stream, **if** we have later on more events happening on request stream, we will have the corresponding response events happening on response stream, as expected: 383 | 384 | ``` 385 | requestStream: --a-----b--c------------|-> 386 | responseStream: -----A--------B-----C---|-> 387 | 388 | (lowercase is a request, uppercase is its response) 389 | ``` 390 | 391 | Now that we finally have a response stream, we can render the data we receive: 392 | 393 | ```javascript 394 | responseStream.subscribe(function(response) { 395 | // render `response` to the DOM however you wish 396 | }); 397 | ``` 398 | 399 | Joining all the code until now, we have: 400 | 401 | ```javascript 402 | var requestStream = Rx.Observable.just('https://api.github.com/users'); 403 | 404 | var responseStream = requestStream 405 | .flatMap(function(requestUrl) { 406 | return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); 407 | }); 408 | 409 | responseStream.subscribe(function(response) { 410 | // render `response` to the DOM however you wish 411 | }); 412 | ``` 413 | 414 | 一个响应的metastream,看起来确实让人容易困惑,看样子对我们一点帮助也没有。我们只想要一个简单的响应数据流,每一个发出的值是一个简单的JSON对象就行,而不是一个'Promise' 的JSON对象。ok,让我们来见识一下另一个函数:[Flatmap](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypeflatmapselector-resultselector),它是`map()`函数的另一个版本,它比metastream更扁平。一切在"主躯干"事件流发出的事件都将在"分支"事件流中发出。Flatmap并不是metastreams的修复版,metastreams也不是一个bug。它俩在Rx中都是处理异步响应事件的好工具、好帮手。 415 | 416 | ```javascript 417 | var responseStream = requestStream 418 | .flatMap(function(requestUrl) { 419 | return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); 420 | }); 421 | ``` 422 | 423 | ![Response stream](images/zresponsestream.png) 424 | 425 | 很赞,因为我们的响应事件流是根据请求事件流定义的,**如果**我们以后有更多事件发生在请求事件流的话,我们也将会在相应的响应事件流收到响应事件,就如所期待的那样: 426 | 427 | ``` 428 | requestStream: --a-----b--c------------|-> 429 | responseStream: -----A--------B-----C---|-> 430 | 431 | (小写的是请求事件流, 大写的是响应事件流) 432 | ``` 433 | 434 | 现在,我们终于有响应的事件流了,并且可以用我们收到的数据来渲染了: 435 | 436 | ```javascript 437 | responseStream.subscribe(function(response) { 438 | // render `response` to the DOM however you wish 439 | }); 440 | ``` 441 | 442 | 让我们把所有代码合起来,看一下: 443 | 444 | ```javascript 445 | var requestStream = Rx.Observable.just('https://api.github.com/users'); 446 | 447 | var responseStream = requestStream 448 | .flatMap(function(requestUrl) { 449 | return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); 450 | }); 451 | 452 | responseStream.subscribe(function(response) { 453 | // render `response` to the DOM however you wish 454 | }); 455 | ``` 456 | 457 | ## The refresh button 458 | 459 | I did not yet mention that the JSON in the response is a list with 100 users. The API only allows us to specify the page offset, and not the page size, so we're using just 3 data objects and wasting 97 others. We can ignore that problem for now, since later on we will see how to cache the responses. 460 | 461 | Everytime the refresh button is clicked, the request stream should emit a new URL, so that we can get a new response. We need two things: a stream of click events on the refresh button (mantra: anything can be a stream), and we need to change the request stream to depend on the refresh click stream. Gladly, RxJS comes with tools to make Observables from event listeners. 462 | 463 | ```javascript 464 | var refreshButton = document.querySelector('.refresh'); 465 | var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click'); 466 | ``` 467 | 468 | Since the refresh click event doesn't itself carry any API URL, we need to map each click to an actual URL. Now we change the request stream to be the refresh click stream mapped to the API endpoint with a random offset parameter each time. 469 | 470 | ```javascript 471 | var requestStream = refreshClickStream 472 | .map(function() { 473 | var randomOffset = Math.floor(Math.random()*500); 474 | return 'https://api.github.com/users?since=' + randomOffset; 475 | }); 476 | ``` 477 | 478 | Because I'm dumb and I don't have automated tests, I just broke one of our previously built features. A request doesn't happen anymore on startup, it happens only when the refresh is clicked. Urgh. I need both behaviors: a request when _either_ a refresh is clicked _or_ the webpage was just opened. 479 | 480 | ## 刷新按钮 481 | 482 | 我还没提到本次响应的JSON数据是含有100个用户数据的list,这个API只允许指定页面偏移量(page offset),而不能指定每页大小(page size),我们只用到了3个用户数据而浪费了其他97个,现在可以先忽略这个问题,稍后我们将学习如何缓存响应的数据。 483 | 484 | 每当刷新按钮被点击,请求事件流就会发出一个新的URL值,这样我们就可以获取新的响应数据。这里我们需要两个东西:点击刷新按钮的事件流(准则:一切都能作为事件流),我们需要将点击刷新按钮的事件流作为请求事件流的依赖(即点击刷新事件流会引起请求事件流)。幸运的是,RxJS已经有了可以从事件监听者转换成被观察者的方法了。 485 | 486 | ```javascript 487 | var refreshButton = document.querySelector('.refresh'); 488 | var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click'); 489 | ``` 490 | 491 | 因为刷新按钮点击事件不会携带将要请求的API的URL,我们需要将每次的点击映射到一个实际的URL上,现在我们将请求事件流转换成了一个点击事件流,并将每次的点击映射成一个随机的页面偏移量(offset)参数来组成API的URL。 492 | 493 | ```javascript 494 | var requestStream = refreshClickStream 495 | .map(function() { 496 | var randomOffset = Math.floor(Math.random()*500); 497 | return 'https://api.github.com/users?since=' + randomOffset; 498 | }); 499 | ``` 500 | 501 | 因为我比较笨而且也没有使用自动化测试,所以我刚把之前做好的一个功能搞烂了。这样,请求在一开始的时候就不会执行,而只有在点击事件发生时才会执行。我们需要的是两种情况都要执行:刚开始打开网页和点击刷新按钮都会执行的请求。 502 | 503 | We know how to make a separate stream for each one of those cases: 504 | 505 | ```javascript 506 | var requestOnRefreshStream = refreshClickStream 507 | .map(function() { 508 | var randomOffset = Math.floor(Math.random()*500); 509 | return 'https://api.github.com/users?since=' + randomOffset; 510 | }); 511 | 512 | var startupRequestStream = Rx.Observable.just('https://api.github.com/users'); 513 | ``` 514 | 515 | But how can we "merge" these two into one? Well, there's [`merge()`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypemergemaxconcurrent--other). Explained in the diagram dialect, this is what it does: 516 | 517 | ``` 518 | stream A: ---a--------e-----o-----> 519 | stream B: -----B---C-----D--------> 520 | vvvvvvvvv merge vvvvvvvvv 521 | ---a-B---C--e--D--o-----> 522 | ``` 523 | 524 | 我们知道如何为每一种情况做一个单独的事件流: 525 | 526 | ```javascript 527 | var requestOnRefreshStream = refreshClickStream 528 | .map(function() { 529 | var randomOffset = Math.floor(Math.random()*500); 530 | return 'https://api.github.com/users?since=' + randomOffset; 531 | }); 532 | 533 | var startupRequestStream = Rx.Observable.just('https://api.github.com/users'); 534 | ``` 535 | 536 | 但是我们是否可以将这两个合并成一个呢?没错,是可以的,我们可以使用[`merge()`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypemergemaxconcurrent--other)方法来实现。下图可以解释`merge()`函数的用处: 537 | 538 | ``` 539 | stream A: ---a--------e-----o-----> 540 | stream B: -----B---C-----D--------> 541 | vvvvvvvvv merge vvvvvvvvv 542 | ---a-B---C--e--D--o-----> 543 | ``` 544 | 545 | It should be easy now: 546 | 547 | ```javascript 548 | var requestOnRefreshStream = refreshClickStream 549 | .map(function() { 550 | var randomOffset = Math.floor(Math.random()*500); 551 | return 'https://api.github.com/users?since=' + randomOffset; 552 | }); 553 | 554 | var startupRequestStream = Rx.Observable.just('https://api.github.com/users'); 555 | 556 | var requestStream = Rx.Observable.merge( 557 | requestOnRefreshStream, startupRequestStream 558 | ); 559 | ``` 560 | 561 | There is an alternative and cleaner way of writing that, without the intermediate streams. 562 | 563 | ```javascript 564 | var requestStream = refreshClickStream 565 | .map(function() { 566 | var randomOffset = Math.floor(Math.random()*500); 567 | return 'https://api.github.com/users?since=' + randomOffset; 568 | }) 569 | .merge(Rx.Observable.just('https://api.github.com/users')); 570 | ``` 571 | 572 | 现在做起来应该很简单: 573 | 574 | ```javascript 575 | var requestOnRefreshStream = refreshClickStream 576 | .map(function() { 577 | var randomOffset = Math.floor(Math.random()*500); 578 | return 'https://api.github.com/users?since=' + randomOffset; 579 | }); 580 | 581 | var startupRequestStream = Rx.Observable.just('https://api.github.com/users'); 582 | 583 | var requestStream = Rx.Observable.merge( 584 | requestOnRefreshStream, startupRequestStream 585 | ); 586 | ``` 587 | 588 | 还有一个更干净的写法,省去了中间事件流变量: 589 | 590 | ```javascript 591 | var requestStream = refreshClickStream 592 | .map(function() { 593 | var randomOffset = Math.floor(Math.random()*500); 594 | return 'https://api.github.com/users?since=' + randomOffset; 595 | }) 596 | .merge(Rx.Observable.just('https://api.github.com/users')); 597 | ``` 598 | 599 | 甚至可以更简短,更具有可读性: 600 | ```javascript 601 | var requestStream = refreshClickStream 602 | .map(function() { 603 | var randomOffset = Math.floor(Math.random()*500); 604 | return 'https://api.github.com/users?since=' + randomOffset; 605 | }) 606 | .startWith('https://api.github.com/users'); 607 | ``` 608 | 609 | The [`startWith()`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypestartwithscheduler-args) function does exactly what you think it does. No matter how your input stream looks like, the output stream resulting of `startWith(x)` will have `x` at the beginning. But I'm not [DRY](https://en.wikipedia.org/wiki/Don't_repeat_yourself) enough, I'm repeating the API endpoint string. One way to fix this is by moving the `startWith()` close to the `refreshClickStream`, to essentially "emulate" a refresh click on startup. 610 | 611 | ```javascript 612 | var requestStream = refreshClickStream.startWith('startup click') 613 | .map(function() { 614 | var randomOffset = Math.floor(Math.random()*500); 615 | return 'https://api.github.com/users?since=' + randomOffset; 616 | }); 617 | ``` 618 | 619 | Nice. If you go back to the point where I "broke the automated tests", you should see that the only difference with this last approach is that I added the `startWith()`. 620 | 621 | [`startWith()`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypestartwithscheduler-args)函数做的事和你预期的完全一样。无论你的输入事件流是怎样的,使用`startWith(x)`函数处理过后输出的事件流一定是一个`x` 开头的结果。但是我没有总是[重复代码( DRY)](https://en.wikipedia.org/wiki/Don't_repeat_yourself),我只是在重复API的URL字符串,改进的方法是将 `startWith()`函数挪到`refreshClickStream`那里,这样就可以在启动时,模拟一个刷新按钮的点击事件了。 622 | 623 | ```javascript 624 | var requestStream = refreshClickStream.startWith('startup click') 625 | .map(function() { 626 | var randomOffset = Math.floor(Math.random()*500); 627 | return 'https://api.github.com/users?since=' + randomOffset; 628 | }); 629 | ``` 630 | 631 | 不错,如果你倒回到"搞烂了的自动测试"的地方,然后再对比这两个地方,你会发现我仅仅是加了一个`startWith()`函数而已。 632 | 633 | ## Modelling the 3 suggestions with streams 634 | 635 | Until now, we have only touched a _suggestion_ UI element on the rendering step that happens in the responseStream's `subscribe()`. Now with the refresh button, we have a problem: as soon as you click 'refresh', the current 3 suggestions are not cleared. New suggestions come in only after a response has arrived, but to make the UI look nice, we need to clean out the current suggestions when clicks happen on the refresh. 636 | 637 | ```javascript 638 | refreshClickStream.subscribe(function() { 639 | // clear the 3 suggestion DOM elements 640 | }); 641 | ``` 642 | 643 | No, not so fast, pal. This is bad, because we now have **two** subscribers that affect the suggestion DOM elements (the other one being `responseStream.subscribe()`), and that doesn't really sound like [Separation of concerns](https://en.wikipedia.org/wiki/Separation_of_concerns). Remember the Reactive mantra? 644 | 645 |   646 |   647 |   648 |   649 | 650 | ![Mantra](http://i.imgur.com/AIimQ8C.jpg) 651 | 652 | ## 用事件流将3个推荐的用户数据模型化 653 | 654 | 直到现在,在响应事件流(responseStream)的订阅(`subscribe()`)函数发生的渲染步骤里,我们只是稍微提及了一下_推荐关注_的UI。现在有了刷新按钮,我们就会出现一个问题:当你点击了刷新按钮,当前的三个推荐关注用户没有被清楚,而只要响应的数据达到后我们就拿到了新的推荐关注的用户数据,为了让UI看起来更漂亮,我们需要在点击刷新按钮的事件发生的时候清楚当前的三个推荐关注的用户。 655 | 656 | ```javascript 657 | refreshClickStream.subscribe(function() { 658 | // clear the 3 suggestion DOM elements 659 | }); 660 | ``` 661 | 662 | 不,老兄,还没那么快。我们又出现了新的问题,因为我们现在有两个订阅者在影响着推荐关注的UI DOM元素(另一个是 `responseStream.subscribe()`),这看起来并不符合[关注分离(Separation of concerns)](https://en.wikipedia.org/wiki/Separation_of_concerns)原则,还记得响应式编程的原则么? 663 | 664 |   665 |   666 |   667 |   668 | 669 | ![Mantra](http://i.imgur.com/AIimQ8C.jpg) 670 | 671 | So let's model a suggestion as a stream, where each emitted value is the JSON object containing the suggestion data. We will do this separately for each of the 3 suggestions. This is how the stream for suggestion #1 could look like: 672 | 673 | ```javascript 674 | var suggestion1Stream = responseStream 675 | .map(function(listUsers) { 676 | // get one random user from the list 677 | return listUsers[Math.floor(Math.random()*listUsers.length)]; 678 | }); 679 | ``` 680 | 681 | The others, `suggestion2Stream` and `suggestion3Stream` can be simply copy pasted from `suggestion1Stream`. This is not DRY, but it will keep our example simple for this tutorial, plus I think it's a good exercise to think how to avoid repetition in this case. 682 | 683 | 现在,让我们把推荐关注的用户数据模型化成事件流形式,每个被发出的值是一个包含了推荐关注用户数据的JSON对象。我们将把这三个用户数据分开处理,下面是推荐关注的1号用户数据的事件流: 684 | 685 | ```javascript 686 | var suggestion1Stream = responseStream 687 | .map(function(listUsers) { 688 | // get one random user from the list 689 | return listUsers[Math.floor(Math.random()*listUsers.length)]; 690 | }); 691 | ``` 692 | 693 | 其他的,如推荐关注的2号用户数据的事件流`suggestion2Stream`和推荐关注的3号用户数据的事件流`suggestion3Stream` 都可以方便的从`suggestion1Stream` 复制粘贴就好。这里并不是**重复代码**,只是为让我们的示例更加简单,而且我认为这是一个思考如何避免**重复代码**的好案例。 694 | 695 | Instead of having the rendering happen in responseStream's subscribe(), we do that here: 696 | 697 | ```javascript 698 | suggestion1Stream.subscribe(function(suggestion) { 699 | // render the 1st suggestion to the DOM 700 | }); 701 | ``` 702 | 703 | Back to the "on refresh, clear the suggestions", we can simply map refresh clicks to `null` suggestion data, and include that in the `suggestion1Stream`, as such: 704 | 705 | ```javascript 706 | var suggestion1Stream = responseStream 707 | .map(function(listUsers) { 708 | // get one random user from the list 709 | return listUsers[Math.floor(Math.random()*listUsers.length)]; 710 | }) 711 | .merge( 712 | refreshClickStream.map(function(){ return null; }) 713 | ); 714 | ``` 715 | 716 | And when rendering, we interpret `null` as "no data", hence hiding its UI element. 717 | 718 | ```javascript 719 | suggestion1Stream.subscribe(function(suggestion) { 720 | if (suggestion === null) { 721 | // hide the first suggestion DOM element 722 | } 723 | else { 724 | // show the first suggestion DOM element 725 | // and render the data 726 | } 727 | }); 728 | ``` 729 | 730 | 我们不在responseStream的subscribe()中处理渲染了,我们这样处理: 731 | 732 | ```javascript 733 | suggestion1Stream.subscribe(function(suggestion) { 734 | // render the 1st suggestion to the DOM 735 | }); 736 | ``` 737 | 738 | 回到"当刷新时,清楚掉当前的推荐关注的用户",我们可以很简单的把刷新点击映射为没有推荐数据(`null` suggestion data),并且在`suggestion1Stream`中包含进来,如下: 739 | 740 | ```javascript 741 | var suggestion1Stream = responseStream 742 | .map(function(listUsers) { 743 | // get one random user from the list 744 | return listUsers[Math.floor(Math.random()*listUsers.length)]; 745 | }) 746 | .merge( 747 | refreshClickStream.map(function(){ return null; }) 748 | ); 749 | ``` 750 | 751 | 当渲染时,我们将 `null`解释为"没有数据",然后把UI元素隐藏起来。 752 | 753 | ```javascript 754 | suggestion1Stream.subscribe(function(suggestion) { 755 | if (suggestion === null) { 756 | // hide the first suggestion DOM element 757 | } 758 | else { 759 | // show the first suggestion DOM element 760 | // and render the data 761 | } 762 | }); 763 | ``` 764 | 765 | The big picture is now: 766 | 767 | ``` 768 | refreshClickStream: ----------o--------o----> 769 | requestStream: -r--------r--------r----> 770 | responseStream: ----R---------R------R--> 771 | suggestion1Stream: ----s-----N---s----N-s--> 772 | suggestion2Stream: ----q-----N---q----N-q--> 773 | suggestion3Stream: ----t-----N---t----N-t--> 774 | ``` 775 | 776 | Where `N` stands for `null`. 777 | 778 | As a bonus, we can also render "empty" suggestions on startup. That is done by adding `startWith(null)` to the suggestion streams: 779 | 780 | ```javascript 781 | var suggestion1Stream = responseStream 782 | .map(function(listUsers) { 783 | // get one random user from the list 784 | return listUsers[Math.floor(Math.random()*listUsers.length)]; 785 | }) 786 | .merge( 787 | refreshClickStream.map(function(){ return null; }) 788 | ) 789 | .startWith(null); 790 | ``` 791 | 792 | Which results in: 793 | 794 | ``` 795 | refreshClickStream: ----------o---------o----> 796 | requestStream: -r--------r---------r----> 797 | responseStream: ----R----------R------R--> 798 | suggestion1Stream: -N--s-----N----s----N-s--> 799 | suggestion2Stream: -N--q-----N----q----N-q--> 800 | suggestion3Stream: -N--t-----N----t----N-t--> 801 | ``` 802 | 803 | 现在我们的一个大的示意图是这样的: 804 | 805 | ``` 806 | refreshClickStream: ----------o--------o----> 807 | requestStream: -r--------r--------r----> 808 | responseStream: ----R---------R------R--> 809 | suggestion1Stream: ----s-----N---s----N-s--> 810 | suggestion2Stream: ----q-----N---q----N-q--> 811 | suggestion3Stream: ----t-----N---t----N-t--> 812 | ``` 813 | 814 | `N`代表`null` 815 | 816 | 作为一种补充,我们可以在一开始的时候就渲染空的推荐内容。这通过把startWith(null)添加到推荐关注的事件流就可以了: 817 | 818 | ```javascript 819 | var suggestion1Stream = responseStream 820 | .map(function(listUsers) { 821 | // get one random user from the list 822 | return listUsers[Math.floor(Math.random()*listUsers.length)]; 823 | }) 824 | .merge( 825 | refreshClickStream.map(function(){ return null; }) 826 | ) 827 | .startWith(null); 828 | ``` 829 | 830 | 结果是这样的: 831 | 832 | ``` 833 | refreshClickStream: ----------o---------o----> 834 | requestStream: -r--------r---------r----> 835 | responseStream: ----R----------R------R--> 836 | suggestion1Stream: -N--s-----N----s----N-s--> 837 | suggestion2Stream: -N--q-----N----q----N-q--> 838 | suggestion3Stream: -N--t-----N----t----N-t--> 839 | ``` 840 | 841 | ## Closing a suggestion and using cached responses 842 | 843 | There is one feature remaining to implement. Each suggestion should have its own 'x' button for closing it, and loading another in its place. At first thought, you could say it's enough to make a new request when any close button is clicked: 844 | 845 | ```javascript 846 | var close1Button = document.querySelector('.close1'); 847 | var close1ClickStream = Rx.Observable.fromEvent(close1Button, 'click'); 848 | // and the same for close2Button and close3Button 849 | 850 | var requestStream = refreshClickStream.startWith('startup click') 851 | .merge(close1ClickStream) // we added this 852 | .map(function() { 853 | var randomOffset = Math.floor(Math.random()*500); 854 | return 'https://api.github.com/users?since=' + randomOffset; 855 | }); 856 | ``` 857 | 858 | That does not work. It will close and reload _all_ suggestions, rather than just only the one we clicked on. There are a couple of different ways of solving this, and to keep it interesting, we will solve it by reusing previous responses. The API's response page size is 100 users while we were using just 3 of those, so there is plenty of fresh data available. No need to request more. 859 | 860 | ## 推荐关注的关闭和使用已缓存的响应数据(responses) 861 | 862 | 只剩这一个功能没有实现了,每个推荐关注的用户UI会有一个'x'按钮来关闭自己,然后在当前的用户数据UI中加载另一个推荐关注的用户。最初的想法是:点击任何关闭按钮时都需要发起一个新的请求: 863 | 864 | ```javascript 865 | var close1Button = document.querySelector('.close1'); 866 | var close1ClickStream = Rx.Observable.fromEvent(close1Button, 'click'); 867 | // and the same for close2Button and close3Button 868 | 869 | var requestStream = refreshClickStream.startWith('startup click') 870 | .merge(close1ClickStream) // we added this 871 | .map(function() { 872 | var randomOffset = Math.floor(Math.random()*500); 873 | return 'https://api.github.com/users?since=' + randomOffset; 874 | }); 875 | ``` 876 | 877 | 这样没什么效果,这样会关闭和重新加载_全部_的推荐关注用户,而不仅仅是处理我们点击的那一个。这里有几种方式来解决这个问题,并且让它变得有趣,我们将重用之前的请求数据来解决这个问题。这个API响应的每页数据大小是100个用户数据,而我们只使用了其中三个,所以还有一大堆未使用的数据可以拿来用,不用去请求更多数据了。 878 | 879 | Again, let's think in streams. When a 'close1' click event happens, we want to use the _most recently emitted_ response on `responseStream` to get one random user from the list in the response. As such: 880 | 881 | ``` 882 | requestStream: --r---------------> 883 | responseStream: ------R-----------> 884 | close1ClickStream: ------------c-----> 885 | suggestion1Stream: ------s-----s-----> 886 | ``` 887 | 888 | In Rx* there is a combinator function called [`combineLatest`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypecombinelatestargs-resultselector) that seems to do what we need. It takes two streams A and B as inputs, and whenever either stream emits a value, `combineLatest` joins the two most recently emitted values `a` and `b` from both streams and outputs a value `c = f(x,y)`, where `f` is a function you define. It is better explained with a diagram: 889 | 890 | ``` 891 | stream A: --a-----------e--------i--------> 892 | stream B: -----b----c--------d-------q----> 893 | vvvvvvvv combineLatest(f) vvvvvvv 894 | ----AB---AC--EC---ED--ID--IQ----> 895 | 896 | where f is the uppercase function 897 | ``` 898 | 899 | ok,再来,我们继续用事件流的方式来思考。当'close1'点击事件发生时,我们想要使用_最近发出的_响应数据,并执行`responseStream`函数来从响应列表里随机的抽出一个用户数据来,就像下面这样: 900 | 901 | ``` 902 | requestStream: --r---------------> 903 | responseStream: ------R-----------> 904 | close1ClickStream: ------------c-----> 905 | suggestion1Stream: ------s-----s-----> 906 | ``` 907 | 908 | 在Rx中一个组合函数叫做[`combineLatest`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypecombinelatestargs-resultselector),应该是我们需要的。这个函数会把数据流A和数据流B作为输入,并且无论哪一个数据流发出一个值了,`combineLatest` 函数就会将从两个数据流最近发出的值`a`和`b`作为`f`函数的输入,计算后返回一个输出值(`c = f(x,y)`),下面的图表会让这个函数的过程看起来会更加清晰: 909 | 910 | ``` 911 | stream A: --a-----------e--------i--------> 912 | stream B: -----b----c--------d-------q----> 913 | vvvvvvvv combineLatest(f) vvvvvvv 914 | ----AB---AC--EC---ED--ID--IQ----> 915 | 916 | f是转换成大写的函数 917 | ``` 918 | 919 | We can apply combineLatest() on `close1ClickStream` and `responseStream`, so that whenever the close 1 button is clicked, we get the latest response emitted and produce a new value on `suggestion1Stream`. On the other hand, combineLatest() is symmetric: whenever a new response is emitted on `responseStream`, it will combine with the latest 'close 1' click to produce a new suggestion. That is interesting, because it allows us to simplify our previous code for `suggestion1Stream`, like this: 920 | 921 | ```javascript 922 | var suggestion1Stream = close1ClickStream 923 | .combineLatest(responseStream, 924 | function(click, listUsers) { 925 | return listUsers[Math.floor(Math.random()*listUsers.length)]; 926 | } 927 | ) 928 | .merge( 929 | refreshClickStream.map(function(){ return null; }) 930 | ) 931 | .startWith(null); 932 | ``` 933 | 934 | One piece is still missing in the puzzle. The combineLatest() uses the most recent of the two sources, but if one of those sources hasn't emitted anything yet, combineLatest() cannot produce a data event on the output stream. If you look at the ASCII diagram above, you will see that the output has nothing when the first stream emitted value `a`. Only when the second stream emitted value `b` could it produce an output value. 935 | 936 | There are different ways of solving this, and we will stay with the simplest one, which is simulating a click to the 'close 1' button on startup: 937 | 938 | ```javascript 939 | var suggestion1Stream = close1ClickStream.startWith('startup click') // we added this 940 | .combineLatest(responseStream, 941 | function(click, listUsers) {l 942 | return listUsers[Math.floor(Math.random()*listUsers.length)]; 943 | } 944 | ) 945 | .merge( 946 | refreshClickStream.map(function(){ return null; }) 947 | ) 948 | .startWith(null); 949 | ``` 950 | 951 | 这样,我们就可以把`combineLatest()`函数用在`close1ClickStream`和 `responseStream`上了,只要关闭按钮被点击,我们就可以获得最近的响应数据,并在`suggestion1Stream`上产生出一个新值。另一方面,`combineLatest()`函数也是相对的:每当在`responseStream`上发出一个新的响应,它将会结合一次新的`点击关闭按钮事件`来产生一个新的推荐关注的用户数据,这非常有趣,因为它可以给我们的`suggestion1Stream`简化代码: 952 | 953 | ```javascript 954 | var suggestion1Stream = close1ClickStream 955 | .combineLatest(responseStream, 956 | function(click, listUsers) { 957 | return listUsers[Math.floor(Math.random()*listUsers.length)]; 958 | } 959 | ) 960 | .merge( 961 | refreshClickStream.map(function(){ return null; }) 962 | ) 963 | .startWith(null); 964 | ``` 965 | 966 | One piece is still missing in the puzzle. The combineLatest() uses the most recent of the two sources, but if one of those sources hasn't emitted anything yet, combineLatest() cannot produce a data event on the output stream. If you look at the ASCII diagram above, you will see that the output has nothing when the first stream emitted value `a`. Only when the second stream emitted value `b` could it produce an output value. 967 | 968 | There are different ways of solving this, and we will stay with the simplest one, which is simulating a click to the 'close 1' button on startup: 969 | 970 | ```javascript 971 | var suggestion1Stream = close1ClickStream.startWith('startup click') // we added this 972 | .combineLatest(responseStream, 973 | function(click, listUsers) {l 974 | return listUsers[Math.floor(Math.random()*listUsers.length)]; 975 | } 976 | ) 977 | .merge( 978 | refreshClickStream.map(function(){ return null; }) 979 | ) 980 | .startWith(null); 981 | ``` 982 | 983 | 现在,我们的拼图还缺一小块地方。`combineLatest()`函数使用了最近的两个数据源,但是如果某一个数据源还没有发出任何东西,`combineLatest()`函数就不能在输出流上产生一个数据事件。如果你看了上面的ASCII图表(文章中第一个图表),你会明白当第一个数据流发出一个值`a`时并没有任何的输出,只有当第二个数据流发出一个值`b`的时候才会产生一个输出值。 984 | 985 | 这里有很多种方法来解决这个问题,我们使用最简单的一种,也就是在启动的时候模拟'close 1'的点击事件: 986 | 987 | ```javascript 988 | var suggestion1Stream = close1ClickStream.startWith('startup click') // we added this 989 | .combineLatest(responseStream, 990 | function(click, listUsers) {l 991 | return listUsers[Math.floor(Math.random()*listUsers.length)]; 992 | } 993 | ) 994 | .merge( 995 | refreshClickStream.map(function(){ return null; }) 996 | ) 997 | .startWith(null); 998 | ``` 999 | 1000 | ## Wrapping up 1001 | 1002 | And we're done. The complete code for all this was: 1003 | 1004 | ```javascript 1005 | var refreshButton = document.querySelector('.refresh'); 1006 | var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click'); 1007 | 1008 | var closeButton1 = document.querySelector('.close1'); 1009 | var close1ClickStream = Rx.Observable.fromEvent(closeButton1, 'click'); 1010 | // and the same logic for close2 and close3 1011 | 1012 | var requestStream = refreshClickStream.startWith('startup click') 1013 | .map(function() { 1014 | var randomOffset = Math.floor(Math.random()*500); 1015 | return 'https://api.github.com/users?since=' + randomOffset; 1016 | }); 1017 | 1018 | var responseStream = requestStream 1019 | .flatMap(function (requestUrl) { 1020 | return Rx.Observable.fromPromise($.ajax({url: requestUrl})); 1021 | }); 1022 | 1023 | var suggestion1Stream = close1ClickStream.startWith('startup click') 1024 | .combineLatest(responseStream, 1025 | function(click, listUsers) { 1026 | return listUsers[Math.floor(Math.random()*listUsers.length)]; 1027 | } 1028 | ) 1029 | .merge( 1030 | refreshClickStream.map(function(){ return null; }) 1031 | ) 1032 | .startWith(null); 1033 | // and the same logic for suggestion2Stream and suggestion3Stream 1034 | 1035 | suggestion1Stream.subscribe(function(suggestion) { 1036 | if (suggestion === null) { 1037 | // hide the first suggestion DOM element 1038 | } 1039 | else { 1040 | // show the first suggestion DOM element 1041 | // and render the data 1042 | } 1043 | }); 1044 | ``` 1045 | 1046 | ## 封装起来 1047 | 1048 | 我们完成了,下面是封装好的完整示例代码: 1049 | 1050 | ```javascript 1051 | var refreshButton = document.querySelector('.refresh'); 1052 | var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click'); 1053 | 1054 | var closeButton1 = document.querySelector('.close1'); 1055 | var close1ClickStream = Rx.Observable.fromEvent(closeButton1, 'click'); 1056 | // and the same logic for close2 and close3 1057 | 1058 | var requestStream = refreshClickStream.startWith('startup click') 1059 | .map(function() { 1060 | var randomOffset = Math.floor(Math.random()*500); 1061 | return 'https://api.github.com/users?since=' + randomOffset; 1062 | }); 1063 | 1064 | var responseStream = requestStream 1065 | .flatMap(function (requestUrl) { 1066 | return Rx.Observable.fromPromise($.ajax({url: requestUrl})); 1067 | }); 1068 | 1069 | var suggestion1Stream = close1ClickStream.startWith('startup click') 1070 | .combineLatest(responseStream, 1071 | function(click, listUsers) { 1072 | return listUsers[Math.floor(Math.random()*listUsers.length)]; 1073 | } 1074 | ) 1075 | .merge( 1076 | refreshClickStream.map(function(){ return null; }) 1077 | ) 1078 | .startWith(null); 1079 | // and the same logic for suggestion2Stream and suggestion3Stream 1080 | 1081 | suggestion1Stream.subscribe(function(suggestion) { 1082 | if (suggestion === null) { 1083 | // hide the first suggestion DOM element 1084 | } 1085 | else { 1086 | // show the first suggestion DOM element 1087 | // and render the data 1088 | } 1089 | }); 1090 | ``` 1091 | 1092 | **You can see this working example at http://jsfiddle.net/staltz/8jFJH/48/** 1093 | 1094 | That piece of code is small but dense: it features management of multiple events with proper separation of concerns, and even caching of responses. The functional style made the code look more declarative than imperative: we are not giving a sequence of instructions to execute, we are just **telling what something is** by defining relationships between streams. For instance, with Rx we told the computer that _`suggestion1Stream` **is** the 'close 1' stream combined with one user from the latest response, besides being `null` when a refresh happens or program startup happened_. 1095 | 1096 | Notice also the impressive absence of control flow elements such as `if`, `for`, `while`, and the typical callback-based control flow that you expect from a JavaScript application. You can even get rid of the `if` and `else` in the `subscribe()` above by using `filter()` if you want (I'll leave the implementation details to you as an exercise). In Rx, we have stream functions such as `map`, `filter`, `scan`, `merge`, `combineLatest`, `startWith`, and many more to control the flow of an event-driven program. This toolset of functions gives you more power in less code. 1097 | 1098 | **你可以在[这里](http://jsfiddle.net/staltz/8jFJH/48/)看到可演示的示例工程** 1099 | 1100 | 以上的代码片段虽小但做到很多事:它适当的使用关注分离(separation of concerns)原则的实现了对多个事件流的管理,甚至做到了响应数据的缓存。这种函数式的风格使得代码看起来更像是声明式编程而非命令式编程:我们并不是在给一组指令去执行,只是定义了事件流之间关系来**告诉它这是什么**。例如,我们用Rx来告诉计算机_`suggestion1Stream`**是**'close 1'事件结合从最新的响应数据中拿到的一个用户数据的数据流,除此之外,当刷新事件发生时和程序启动时,它就是`null`_。 1101 | 1102 | 留意一下代码中并未出现例如`if`, `for`, `while`等流程控制语句,或者像JavaScript那样典型的基于回调(callback-based)的流程控制。如果可以的话(稍候会给你留一些实现细节来作为练习),你甚至可以在`subscribe()`上使用 `filter()`函数来摆脱`if`和`else`。在Rx里,我们有例如: `map`, `filter`, `scan`, `merge`, `combineLatest`, `startWith`等数据流的函数,还有很多函数可以用来控制**事件驱动编程(event-driven program)**的流程。这些函数的集合可以让你使用更少的代码实现更强大的功能。 1103 | 1104 | ## What comes next 1105 | 1106 | If you think Rx* will be your preferred library for Reactive Programming, take a while to get acquainted with the [big list of functions](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md) for transforming, combining, and creating Observables. If you want to understand those functions in diagrams of streams, take a look at [RxJava's very useful documentation with marble diagrams](https://github.com/Netflix/RxJava/wiki/Creating-Observables). Whenever you get stuck trying to do something, draw those diagrams, think on them, look at the long list of functions, and think more. This workflow has been effective in my experience. 1107 | 1108 | Once you start getting the hang of programming with Rx*, it is absolutely required to understand the concept of [Cold vs Hot Observables](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/creating.md#cold-vs-hot-observables). If you ignore this, it will come back and bite you brutally. You have been warned. Sharpen your skills further by learning real functional programming, and getting acquainted with issues such as side effects that affect Rx*. 1109 | 1110 | But Reactive Programming is not just Rx*. There is [Bacon.js](http://baconjs.github.io/) which is intuitive to work with, without the quirks you sometimes encounter in Rx*. The [Elm Language](http://elm-lang.org/) lives in its own category: it's a Functional Reactive Programming **language** that compiles to JavaScript + HTML + CSS, and features a [time travelling debugger](http://debug.elm-lang.org/). Pretty awesome. 1111 | 1112 | Rx works great for event-heavy frontends and apps. But it is not just a client-side thing, it works great also in the backend and close to databases. In fact, [RxJava is a key component for enabling server-side concurrency in Netflix's API](http://techblog.netflix.com/2013/02/rxjava-netflix-api.html). Rx is not a framework restricted to one specific type of application or language. It really is a paradigm that you can use when programming any event-driven software. 1113 | 1114 | If this tutorial helped you, [tweet it forward](https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fgist.github.com%2Fstaltz%2F868e7e9bc2a7b8c1f754%2F&text=The%20introduction%20to%20Reactive%20Programming%20you%27ve%20been%20missing&tw_p=tweetbutton&url=https%3A%2F%2Fgist.github.com%2Fstaltz%2F868e7e9bc2a7b8c1f754&via=andrestaltz). 1115 | 1116 | ## 接下来 1117 | 1118 | 如果你认为Rx将会成为你首选的响应式编程库,接下来就需要花一些时间来熟悉[一大批的函数](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md)用来变形、联合和创建被观察者。如果你想在事件流的图表当中熟悉这些函数,那就来看一下这个:[RxJava's very useful documentation with marble diagrams](https://github.com/Netflix/RxJava/wiki/Creating-Observables)。请记住,无论何时你遇到问题,可以画一下这些图,思考一下,看一看这一大串函数,然后继续思考。以我个人经验,这样效果很有效。 1119 | 1120 | 一旦你开始使用了Rx编程,请记住,理解[Cold vs Hot Observables](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/creating.md#cold-vs-hot-observables)的概念是非常必要的,如果你忽视了这一点,它就会反弹回来并残忍的反咬你一口。我这里已经警告你了,学习函数式编程可以提高你的技能,熟悉一些常见问题,例如Rx会带来的副作用 1121 | 1122 | 但是响应式编程库并不仅仅是Rx,还有相对容易理解的,没有Rx那些怪癖的[Bacon.js](http://baconjs.github.io/)。[Elm Language](http://elm-lang.org/)则以它自己的方式支持响应式编程:它是一门会编译成Javascript + HTML + CSS的响应式编程语言,并有一个[time travelling debugger](http://debug.elm-lang.org/)功能,很棒吧。 1123 | 1124 | 而Rx对于像前端和App这样需要处理大量的编程效果是非常棒的。但是它不只是可以用在客户端,还可以用在后端或者接近数据库的地方。事实上,[RxJava就是Netflix服务端API用来处理并行的组件](http://techblog.netflix.com/2013/02/rxjava-netflix-api.html)。Rx并不是局限于某种应用程序或者编程语言的框架,它真的是你编写任何事件驱动程序,可以遵循的一个非常棒的编程范式。 1125 | 1126 | 如果这篇教程对你有帮助, [那么就请来转发一下吧(tweet it forward)](https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fgist.github.com%2Fstaltz%2F868e7e9bc2a7b8c1f754%2F&text=The%20introduction%20to%20Reactive%20Programming%20you%27ve%20been%20missing&tw_p=tweetbutton&url=https%3A%2F%2Fgist.github.com%2Fstaltz%2F868e7e9bc2a7b8c1f754&via=andrestaltz). 1127 | -------------------------------------------------------------------------------- /others/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyaoo/android-tech-frontier/1ccbf51672db6ccdccf30ae46f41c0c05c9a80b4/others/.DS_Store -------------------------------------------------------------------------------- /others/readme.md: -------------------------------------------------------------------------------- 1 | 优秀外文 2 | --- 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /others/一种更清晰的Android架构/readme.md: -------------------------------------------------------------------------------- 1 | 一种更清晰的Android架构 2 | --- 3 | 4 | > 5 | * 原文链接 : [Architecting Android…The clean way?](http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/) 6 | * 译者 : [Mr.Simple & Sophie.Ping](https://www.github.com/bboyfeiyu) 7 | 过去几个月以来,通过在Tuenti网站上与@pedro_g_s和@flipper83(安卓开发两位大牛)进行友好讨论之后,我决定写这篇关于架构安卓应用的文章。 8 | 我写这篇文章的目的是想把我在过去几个月体悟到的小方法以及在调查和应用中学到的有用的东西分享给大家。 ## 入门指南 大家都知道要写一款精品软件是有难度且很复杂的:不仅要满足特定要求,而且软件还必须具有稳健性,可维护、可测试性强,并且能够灵活适应各种发展与变化。这时候,“清晰架构”就应运而生了,这一架构在开发任何软件应用的时候用起来非常顺手。 这个思路很简单:简洁架构 意味着产品系统中遵循一系列的习惯原则: 9 | 10 | * 框架独立性 11 | * 可测试 12 | * UI独立性 13 | * 数据库独立性 14 | * 任何外部代理模块的独立性 15 | ![arch](https://camo.githubusercontent.com/dd69e725f30c30031dea279adc5a9d09ea3432f2/687474703a2f2f6665726e616e646f63656a61732e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031342f30392f636c65616e5f617263686974656374757265312e706e67) 我们并不要求一定要用四环结构(如图所示),这只是一个示例图解,但是要考虑的是依赖项规则:源码依赖项只能向内指向,内环里的所有项不能了解外环所发生的东西。 16 | 以下是更好地理解和熟悉本方法的一些相关词汇: * Entities:是指一款应用的业务对象 * Use cases:是指结合数据流和实体中的用例,也称为Interactor * Interface Adapters: 这一组适配器,是负责以最合理的格式转换用例(use cases)和实体(entities)之间的数据,表现层(Presenters )和控制层(Controllers ),就属于这一块的。 17 | * Frameworks and Drivers: 这里是所有具体的实现了:比如:UI,工具类,基础框架,等等。 想要更具体,更生动丰富的解释,可以参考[这篇文章](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html)或者[这个视频](https://vimeo.com/43612849)。 18 | ## 场景 我会设置一个简单的场景来开始:创建一个简单的小app,app中显示从云端获取的一个朋友或用户列表。当点击其中任何一个时,会打开一个新的窗口,显示该用户的详细信息。这里我放了一段视频,大家看看[这个视频 (需翻墙)](http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/)大概就可以对我所描述的东西了解个大概了。 19 | ## Android应用架构 这一对象遵循关注分离原则,也就是通过业务规则让内环操作对外环事物一无所知,这样一来,在测试时它们就不会依赖任何的外部元素了。 要达到这个目的,我的建议就是把一个项目分成三个层次,每个层次拥有自己的目的并且各自独立于堆放运作。 值得一提的是,每一层次使用其自有的数据模型以达到独立性的目的(大家可以看到,在代码中需要一个数据映射器来完成数据转换。如果你不想把你的模型和整个应用交叉使用,这是你要付出的代价)。 20 | 以下是图解,大家感受下: 21 | ![schema](http://fernandocejas.com/wp-content/uploads/2014/09/clean_architecture_android.png) 22 | > 注:我并没有使用任何的外部库(除了用于json数据句法分析的gson和用于测试的junit, mockito, robolectric和espresso以外)。原因是它可以使这个示例更清晰。总之,在存储磁盘数据时,记得加上ORM、依赖注入框架或者你熟悉的任何工具或库,这些都会带来很大帮助。(记住:重复制造轮子可不是明智的选择) 23 | ## 表现层 (Presentation Layer) 24 | 表现层在此,表现的是与视图和动画相关的逻辑。这里仅用了一个Model View Presenter(下文简称MVP),但是大家也可以用MVC或MVVM等模式。这里我不再赘述细节,但是需要强调的是,这里的fragment和activity都是View,其内部除了UI逻辑以外没有其他逻辑,这也是所有渲染的东西发生的地方。 本层次的Presenter由多个interactor(用例)组成,用于完成Android UI线程以外的新线程的工作,并借助渲染到view中的数据callback函数来返回。 ![mvp](http://fernandocejas.com/wp-content/uploads/2014/09/clean_architecture_mvp.png) 25 | 如果你需要一个使用MVP和MVVM的[Effective Android UI](https://github.com/pedrovgs/EffectiveAndroidUI/)典型案例,可以参考我朋友Pedro Gómez的文章。 ## 领域层 (Domain Layer) 26 | 这里的业务规则是指所有在本层发生的逻辑。对于Android项目来说,大家还可以看到所有的interactor(用例)实施。这一层是纯粹的java模块,没有任何的Android依赖性。当涉及到业务对象时,所有的外部组件都使用接口。 27 | ![domain](http://fernandocejas.com/wp-content/uploads/2014/09/clean_architecture_domain.png) 28 | ## 数据层 (Data Layer) 应用所需的所有数据都来自这一层中的UserRepository实现(接口在领域层)。这一实现采用了[Repository Pattern](http://martinfowler.com/eaaCatalog/repository.html),主要策略是通过一个工厂根据一定的条件选取不同的数据来源。 比如,通过ID获取一个用户时,如果这个用户在缓存中已经存在,则硬盘缓存数据源会被选中,否则会通过向云端发起请求获取数据,然后存储到硬盘缓存。 这一切背后的原理是由于原始数据对于客户端是透明的,客户端并不关心数据是来源于内存、硬盘还是云端,它需要关心的是数据可以正确地获取到。 ![data](http://fernandocejas.com/wp-content/uploads/2014/09/clean_architecture_data.png) 29 | > 注:在代码方面,出于学习目的,我通过文件系统和Android preference实现了一个简单、原始的硬盘缓存。请记住,如果已经存在了能够完成这些工作的库,就不要重复制造轮子。 ## 错误处理 这是一个长期待解决的讨论话题,如果大家能够分享各自的解决方案,那真真是极好的。 我的策略是使用回调,这样的话,如果数据仓库发生了变化,回调有两个方法:onResponse()和onError(). onError方法将异常信息封装到一个ErrorBundle对象中: 这种方法的难点在于这其中会存在一环扣一环的回调链,错误会沿着这条回调链到达展示层。因此会牺牲一点代码的可读性。另外,如果出现错误,我本来可以通过事件总线系统抛出事件,但是这种实现方式类似于使用C语言的goto语法。在我看来,当你订阅多个事件时,如果不能很好的控制,你可能会被弄得晕头转向。 ## 测试 关于测试方面,我根据不同的层来选择不同的方法: 30 | * 展示层 ( Presentation Layer) : 使用android instrumentation和 espresso进行集成和功能测试 * 领域层 ( Domain Layer) : 使用JUnit和Mockito进行单元测试; * 数据层 ( Data Layer) : 使用Robolectric ( 因为依赖于Android SDK中的类 )进行集成测试和单元测试。 ## 代码展示 我猜你现在在想,扯了那么久的淡,代码究竟在哪里呢? 好吧,这就是你可以找到上述解决方案的[github链接](https://github.com/android10/Android-CleanArchitecture)。还要提一点,在文件夹结构方面,不同的层是通过以下不同的模块反应的: 31 | * presentation: 展示层的Android模块 * domain: 一个没有android依赖的java模块 * data: 一个数据获取来源的android模块。 * data-test: 数据层测试,由于使用Robolectric 存在一些限制,所以我得再独立的java模块中使用。 ## 结论 正如 Bob大叔 所说:“Architecture is About Intent, not Frameworks” ,我非常同意这个说法,当然了,有很多不同的方法做不同的事情(不同的实现方法),我很确定,你每天(像我一样)会面临很多挑战,但是遵循这些方法,可以确保你的应用会: 32 | * 易维护 Easy to maintain * 易测试 Easy to tes. * 高内聚 Very cohesive. * 低耦合 Decoupled. 最后,我强烈推荐你去实践一下,并且分享你的经验。也许你会找到更好的解决方案:我们都知道,不断提升自己是一件件非常好的事。我希望这篇文章对你有所帮助,欢迎拍砖。 ## 参考资料
    33 |
  1. Source code: https://github.com/android10/Android-CleanArchitecture
  2. 34 |
  3. The clean architecture by Uncle Bob
  4. 35 |
  5. Architecture is about Intent, not Frameworks
  6. 36 |
  7. Model View Presenter
  8. 37 |
  9. Repository Pattern by Martin Fowler
  10. 38 |
  11. Android Design Patterns Presentation
  12. 39 |
-------------------------------------------------------------------------------- /template.md: -------------------------------------------------------------------------------- 1 | `这里写中文标题` 2 | --- 3 | 4 | > 5 | * 原文链接 : [原文标题](原文url) 6 | * 译者 : [这里写你的github用户名](github链接) 7 | * 校对者: [这里校对者的github用户名](github链接) 8 | * 状态 : 未完成 / 校对中 / 完成 9 | 10 | 11 | 12 | 13 | `这里是翻译原文,注意翻译时英文和译文都要留在该文档中,并且是一段英文原文下面直接跟着写译文,便于校对。如下示例 : ` 14 | 15 | Over the last months and after having friendly discussions at Tuenti with colleagues like @pedro_g_s and @flipper83 (by the way 2 badass of android development), I have decided that was a good time to write an article about architecting android applications. 16 | The purpose of it is to show you a little approach I had in mind in the last few months plus all the stuff I have learnt from investigating and implementing it. 17 | 18 | 过去几个月以来,通过在Tuenti网站上与@pedro_g_s和@flipper83(安卓开发两位大牛)进行友好讨论之后,我决定写这篇关于架构安卓应用的文章。 19 | 我写这篇文章的目的是想把我在过去几个月体悟到的小方法以及在调查和应用中学到的有用的东西分享给大家。 20 | Getting Started 21 | We know that writing quality software is hard and complex: It is not only about satisfying requirements, also should be robust, maintainable, testable, and flexible enough to adapt to growth and change. This is where “the clean architecture” comes up and could be a good approach for using when developing any software application. 22 | The idea is simple: clean architecture stands for a group of practices that produce systems that are: 23 | 24 | ## 入门指南 大家都知道要写一款精品软件是有难度且很复杂的:不仅要满足特定要求,而且软件还必须具有稳健性,可维护、可测试性强,并且能够灵活适应各种发展与变化。这时候,“清晰架构”就应运而生了,这一架构在开发任何软件应用的时候用起来非常顺手。 这个思路很简单:简洁架构 意味着产品系统中遵循一系列的习惯原则: 25 | 26 | 27 | 28 | --------------------------------------------------------------------------------