{
public void attachView(V view);
public void detachView(boolean retainInstance);
}
```
上文提到,我们把**Activity**和**Fragment**看做View。因此Mosby库的MVP模块提供了 属于**MvpViews** 的**MvpActivity**和**MvpFragment**作为**Activity**和**Fragment**的基类。
```
public abstract class MvpActivity extends MosbyActivity implements MvpView{
protected P presenter;
@Override protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
presenter = createPresenter();
presenter.attachView(this);
super.onDestroy();
presenter.detachView(false);
}
protected abstract PcreatePresenter();
}
public abstract class MvpFragment
MosbyFragment implements MvpView{
protected Ppresenter;
@Override public void onViewCreated(View view,@Nullable Bundle savedInstanceState){
super.onViewCreated(view,savedInstanceState);
// Create the presenter if needed
if(presenter == null){
presenter = createPresenter();
}
presenter.attachView(this);
}
@Override public void onDestroyView(){
super.onDestroyView();
presenter.detachView(getRetainInstance());
}
protected abstract PcreatePresenter();
}
}
@Override protected void onDestroy(){
```
这一理念主要是一个**MvpView** (也就是Fragment or Activity)会关联一个MvpPresenter,并且管理**MbpPresenter**的声明周期。大家从上面的代码片段可以看到,Mosby使用Activity和Fragement生命周期来实现这一目的。通常presenter是绑定在该生命周期上的。所以初始化或者清理一些东西等操作(例如撤销异步运行任务)应该在 **presenter.onAttach()**和 presenter.onDetach()上进行。我们稍后会谈到presenter如何使用setRetainInstanceState(true) “避开”Fragment中的生命周期。我相信大家也注意到了, MvpPresenter是一个interface 。MVP模块提供一个 **MvpBasePresenter**,这个**MvpBasePresenter**只持有View(是一个Fragment或Activity)的弱引用,从而避免内存泄露。因此,当**presenter**想要调用view方法时,我们需要查看**isViewAttached()** 并使用**getView()**来获取引用,以检查view是否连接到了presenter。
##Loading-Content-Error (LCE)
通常Fragment会一直重复做某一件事。它在后台加载数据,同时显示加载view(即ProgressBar),并在屏幕上显示加载的数据,或者当加载失败时显示view错误。如今,下拉刷新支持很容易实现,因为SwipeRefreshLayout是Android支持库的组成部分。为了避免重复执行这一工作流,Mosby库的**MVP**模块提供了**MvpLceView**。
```
public interface MvpLceView extends MvpView{
/**
* 显示一个加载中的视图
* loading view 必须有个id 为 R.id.loadingView的View
* @param pullToRefresh 如果是true,那么表示下拉刷新被触发了
*/
public void showLoading(boolean pullToRefresh);
/**
* 显示 content view.
* >{
}
```
为什么要为View定义接口呢?
1.因为定义了这个接口之后我们可以更改view的实现。我们可以简单地把代码从一个继承自 Activity的实现转移到继承自 Fragment的实现。
2.模块性:我们可以移动独立的库项目中的整个业务逻辑层、Presenter以及View 接口,然后把这个包含了Presenter的库应用到各类app当中。下图中左侧是使用了嵌入在ViewPager中的Activity的**kicker app**,以及使用嵌入在ViewPager中的Fragment的**meinVerein app**,如图1-5。 两者采用的是同一个定义了View接口和Presenter且测试了单元的库。

由于我们可以通过执行view接口来模拟view,所以我们可以很容易地编写单元测试。还有一个更简单的方法就是在presenter中引入java接口并使用模拟presenter对象来编写单元测试。
还有一个良性副作用就是,定义了view接口之后,我们不用直接从presenter再回调activity/fragment方法。我们这样区分开来是因为在执行presenter时我们在IDE自动完成上看到的方法只是关于view接口的方法。就我个人体会来说,我觉得这个方法非常有用,特别是团队一起工作的时候。需要注意的是,除了定义一个CountriesView接口之外,我们还可以采用**MvpLceView>** 。但是,定义一个专门的接口可以提高代码可读性,并且将来可以灵活地定义更多其他的与View相关的方法。
Next we define our views xml layout file with the required ids:
下一步我们需要按照指定的id来定义view xml 布局文件.
```xml
```
CountriesPresenter控制CountriesView并运行CountriesAsyncLoader。
```java
public class CountriesPresenter extends MvpBasePresenter{
@Override
public void loadCountries(final boolean pullToRefresh){
getView().showLoading(pullToRefresh);
CountriesAsyncLoader countriesLoader = new CountriesAsyncLoader(
new CountriesAsyncLoader.CountriesLoaderListener(){
@Override public void onSuccess(List countries){
if(isViewAttached()){
getView().setData(countries);
getView().showContent();
}
}
@Override public void onError(Exception e){
if(isViewAttached()){
getView().showError(e,pullToRefresh);
}
}
});
countriesLoader.execute();
}
}
```
实现**CountriesView**接口 的**CountriesFragment** 如下所示:
```java
public class CountriesFragment
extends MvpLceFragment,CountriesView,CountriesPresenter>
implements CountriesView,SwipeRefreshLayout.OnRefreshListener{
@InjectView(R.id.recyclerView)RecyclerViewrecyclerView;
CountriesAdapteradapter;
@Override public void onViewCreated(View view,@Nullable Bundle savedInstance){
super.onViewCreated(view,savedInstance);
// Setup contentView == SwipeRefreshView
contentView.setOnRefreshListener(this);
// Setup recycler view
adapter = new CountriesAdapter(getActivity());
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setAdapter(adapter);
loadData(false);
}
public void loadData(boolean pullToRefresh){
presenter.loadCountries(pullToRefresh);
}
@Override protected CountriesPresenter createPresenter(){
return new SimpleCountriesPresenter();
}
// Just a shorthand that will be called in onCreateView()
@Override protected int getLayoutRes(){
return R.layout.countries_list;
}
@Override public void setData(List data){
adapter.setCountries(data);
adapter.notifyDataSetChanged();
}
@Override public void onRefresh(){
loadData(true);
}
}
```
代码数量也并不是很多嘛,对吧?这是因为基类已经执行了从加载view到content view或error view的转换。我们可能第一眼看到那一列**MvpLceFragment**类属参数会觉得灰心。但是我要解释一下:第一种类属参数代表的是content view的类型;第二种是指以fragment显示的Model;第三种是View接口;最后一种是Presenter的类型。总结起来就是:**MvpLceFragment**。
大家可能还注意到的一个点就是 **getLayoutRes()**,它是**MosbyFragment**引入的用于解析xml view布局的速记法。
```java
@Override public View onCreateView(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState){
Return inflater.inflate(getLayoutRes(),container,false);
}
```
因此,我们不用重写**onCreateView()**,只需重写**getLayoutRes()**。一般来说,onCreateView()只能创建view而**onViewCreated()**需要被重写,以便为RecyclerView初始化Adapter等项。因此,千万不要忘记调用super.OnViewCreated();
##ViewState模块
看到这里大家应该大概了解了如何运用Mosby库。Mosby中的ViewState模块能帮助我们在Android开发中解决一些棘手的难题:处理屏幕旋转。
问:如果把正在运行country这个例子的app并显示了一列country的设备从横屏旋转到竖屏,会出现什么情况?
答:大家到这个视频链接[https://youtu.be/9iSBGEIZmUw](https://youtu.be/9iSBGEIZmUw)中看看,结果是一个新的 CountriesFragment会被实例化,app开始显示ProgressBar(并重新加载country列表)而不再在RecyclerView中显示country列表(屏幕旋转前的状态)
Mosby引入了**ViewState**来解决这个问题。原理就是,我们跟踪presenter从关联的View中调用的方法。比如,**presenter**调用的是view.showContent(),一旦showContent()被调用,view就会意识到其状态变更为**“showing content”**,从而view把这一信息存储到一个ViewState。如果view在方向改变过程中遭到破坏,那么ViewState 就会被存储到**Activity.onSaveInstanceState(Bundle) 或 Fragment.onSaveInstanceState(Bundle)中,并在Activity.onCreate(Bundle) 或Fragment.onActivityCreated(Bundle)**中修复。
由于不是每种数据都能存储在Bundle中,所以不同的数据类型采用不同的ViewState 实现:数据类型**ArrayList**采用**ArrayListLceViewState**;数据类型Parcelable 采用**Parcelable DataLceViewState**;数据类型**Serializeable**采用**SerializeableLceViewState**。如果使用的是一个可保持( Retaining )的Fragment,那么 ViewState在屏幕旋转时不会被破坏,所以也就不需要存储到Bundle中。因此,它可以存储任何类型的数据。在这种情况下,我们需要使用**RetainingFragmentLceViewState**。存储一个ViewState比较容易。由于我们的架构比较整洁,我们的View又有接口,ViewState 可以向presenter一样通过调用同样的接口方法来修复相关联的view。举个例子,MvpLceView一般有3种状态,即:显示**showContent(),showLoading()和showError()**,所以ViewState本身会调用相应的方法来修复view的状态。
那只是一些内部构件。如果大家想编写自定义的ViewState,了解以上内容就够了。ViewStates的使用非常简单。事实上,要把MvpLceFragment 迁移到MvpLceViewStateFragment ,我们只需要另外执行**createViewState()** 和 **getData()**。下面我们就在CountriesFragment中实践一下吧:
```java
public class CountriesFragment
extends MvpLceViewStateFragment,CountriesView,CountriesPresenter>
implements CountriesView,SwipeRefreshLayout.OnRefreshListener{
@InjectView(R.id.recyclerView)RecyclerView recyclerView;
CountriesAdapter adapter;
@Override public LceViewState,CountriesView> createViewState(){
return new RetainingFragmentLceViewState,CountriesView>(this);
}
@Override public List getData(){
return adapter == null? null : adapter.getCountries();
}
// The code below is the same as before
@Override public void onViewCreated(Viewview,@Nullable Bundle savedInstance){
super.onViewCreated(view,savedInstance);
// Setup contentView == SwipeRefreshView
contentView.setOnRefreshListener(this);
// Setup recycler view
adapter = new CountriesAdapter(getActivity());
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setAdapter(adapter);
loadData(false);
}
public void loadData(boolean pullToRefresh){
presenter.loadCountries(pullToRefresh);
}
@Override protected CountriesPresenter createPresenter(){
return new SimpleCountriesPresenter();
}
// Just a shorthand that will be called in onCreateView()
@Override protected int getLayoutRes(){
return R.layout.countries_list;
}
@Override public void setData(List data){
adapter.setCountries(data);
adapter.notifyDataSetChanged();
}
@Override public void onRefresh(){
loadData(true);
}
}
```
以上就是全部过程啦。我们不必更改presenter或其他代码。[这里](https://youtu.be/9iSBGEIZmUw ) 是一个关于我们的获得ViewState支持的CountriesFragment的视频。在这个视频中我们可以看到,view在方位转变之后仍然处于同样的“状态”,即,view横屏显示country列表,随后横屏显示country列表。View能横屏显示下拉刷新指示,变更为竖屏时也能显示。
##自定义ViewState
ViewState确实是一个强大且灵活的概念。看到这里我相信大家都了解了LCE (Loading-Content-Error) ViewState的易用性。下面我们就一起来编写自己的View和ViewState吧。我们的View只显示两类不同的数据对象:A和B。结果应该像这个视频 [https://youtu.be/9iSBGEIZmUw](https://youtu.be/9iSBGEIZmUw ) 中演示的这样:
大家心里肯定觉得,这也不怎么样啊!别介啊,我只是想演示一下创建自己的ViewState是一件多么容易的事。
View 接口和数据对象(model)如下所示:
```java
public class A implements Parcelable {
String name;
public A(String name){
this.name=name;
}
public String getName(){
return name;
}
}
public class B implements Parcelable {
String foo;
public B(String foo){
this.foo=foo;
}
public String getFoo(){
return foo;
}
}
public interface MyCustomView extends MvpView{
public void showA(A a);
public void showB(B b);
}
```
在这个简单的例子中我们没有加入业务逻辑层。因为我们假设在实际的app中如果有业务逻辑层的话会使整个生成A或B的操作变得复杂。**Presenter**如下所示:
```java
public class MyCustomPresenter extends MvpBasePresenter{
Random random = new Random();
public void doA(){
A a = new A("My name is A "+random.nextInt(10));
if(isViewAttached()){
getView().showA(a);
}
}
public void doB(){
B b = new B("I am B "+random.nextInt(10));
if(isViewAttached()){
getView().showB(b);
}
}
}
```
我们定义了实现了MyCustomView接口的**MyCustomActivity**。
```java
public class MyCustomActivity extends MvpViewStateActivity
implements MyCustomView{
@InjectView(R.id.textViewA) TextViewaView;
@InjectView(R.id.textViewB) TextViewbView;
@Override protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.my_custom_view);
}
@Override public RestoreableViewState createViewState(){
return new MyCustomViewState();// Our ViewState implementation
}
// Will be called when no view state exist yet,
// which is the case the first time MyCustomActivity starts
@Override public void onNew ViewStateInstance(){
presenter.doA();
}
@Override protected MyCustomPresenter createPresenter(){
return new MyCustomPresenter();
}
@Override public void showA(A a){
MyCustomViewState vs = ((MyCustomViewState)viewState);
vs.setShowingA(true);
vs.setData(a);
aView.setText(a.getName());
aView.setVisibility(View.VISIBLE);
bView.setVisibility(View.GONE);
}
@Override public void showB(B b){
MyCustomViewState vs=((MyCustomViewState)viewState);
vs.setShowingA(false);
vs.setData(b);
bView.setText(b.getFoo());
aView.setVisibility(View.GONE);
bView.setVisibility(View.VISIBLE);
}
@OnClick(R.id.loadA)public void onLoadAClicked(){
presenter.doA();
}
@OnClick(R.id.loadB)public void onLoadBClicked(){
presenter.doB();
}
}
```
由于我们没有LCE(Loading-Content-Error),所以不把 **MvpLceActivity**作为基类。我们采用的是最普遍的支持 ViewState的**MvpViewStateActivity**作为基类。基本上我们的View只显示aView 或 bView。
在**onNew ViewStateInstance()**中,我们需要明确在第一个Activity运行时需要做什么,因为先前并不存在ViewState 例子用于修复。在showA(A a) 和 showB(B b)中,我们需要将显示A 或 B的信息存储到ViewState。到这一步,我们就差不多完成了,现在只差MyCustomViewState执行这一步啦:
```java
ublic class MyCustomViewState implements RestoreableViewState{
private final String KEY_STATE="MyCustomViewState-flag";
private final String KEY_DATA="MyCustomViewState-data";
public boolean showingA=true;// if false, then show B
public Parcelable data;// Can be A or B
@Override public void saveInstanceState(Bundle out){
out.putBoolean (KEY_STATE,showingA);
out.putParcelable (KEY_DATA,data);
}
@Override public boolean restoreInstanceState(Bundle in){
if(in==null){
return false;
}
showingA = in.getBoolean (KEY_STATE,true);
data = in.getParcelable (KEY_DATA);
return true;
}
@Override public void apply(MyCustomView view,boolean retained){
if(showingA){
view.showA((A)data);
}else{
view.showB((B)data);
}
}
/**
* @param a true if showing a, false if showing b
*/
public void setShowingA(boolean a){
this.showingA=a;
}
public void setData(Parcelable data){
this.data=data;
}
}
```
大家可以看到,我们需要把**ViewState**保存到从**Activity.onSaveInstanceState()**调用的 **saveInstanceState()**中,并且在从Activity.onCreate()调用的**restoreInstanceState()**中修复viewstate的数据。apply()方法将会从Activity中调用以修复view state。我们像presenter一样通过调用同样的View interface 方法showA() 或 showB()来实现这一操作。
大家可以看到,我们需要把ViewState保存到从**Activity.onSaveInstanceState()**调用的 **saveInstanceState()**中,并且在从Activity.onCreate()调用的**restoreInstanceState()**中修复viewstate的数据。apply()方法将会从Activity中调用以修复view state。我们像presenter一样通过调用同样的View interface 方法showA() 或 showB()来实现这一操作。
这个外部的**ViewState**把view state修复的复杂性和职责从Activity代码中剥离,并入到这个单独的类中。而编写**ViewState**类的单元测试要比Activity类的单元测试容易得多。
##怎样处理后台线程?
通常,**Presenter**会管理后台线程。Presenter如何处理后台线程取决于它所关联的Activity或者Fragment ,具体分为两种情况:
* 可保持的Fragment : 如果你调用了Fragment的setRetainInstanceState(true)那么这个Fragment在屏幕旋转时就不会被销毁。只有该Fragment的GUI会被销毁,并且在屏幕旋转时重新调用onCreateView创建视图。这就是说当屏幕旋转时Fragment所有的成员成员变量和Presenter不会发生变化。在这个示例中,我们将新的视图关联到Presenter中。因此,Presenter不需要去掉任何正在运行中的后台任务,因为Presenter已经关联了新的视图。例如:
1.竖屏情况下启动应用
2.实例化Fragment时会调用onCreate()、onCreateView()、createPresenter(), 然后通过调用presenter的attachView()函数将View关联到Presenter中。
3. 下一步我们旋转手机屏幕,从竖屏切换到横屏;
4. 此时onDestroyView() 会调用,而onDestroyView() 又会调用presenter的detachView(true)函数。我们注意到detachView有个参数为true,这是告诉presenter这个Fragment是可持有的Fragment(否则这个参数应该为false)。通过这个参数,presenter就知道它不需要取消正在运行的后台任务;
5. 应用现在是横屏状态了,在旋转时onCreateView方法会被调用,但是createPresenter()函数不会被调用,因为我们会对presenter 进行不为空的判断,当presenter为空时才调用createPresenter()函数。而Fragment的setRetainInstanceState(true)会保持这个presenter对象,因此presenter此时不会被重新创建;
6. 在调用了presenter的attachView()之后新创建的View会被重新关联到presenter中。
7. ViewState会被恢复,但是没有后台任务会被取消,因此也没有后台任务需要重新启动。
* Activity和不保持的Fragment :在这个示例中工作流非常的简单。所有的东西都会被销毁,包括presenter。因此presenter对象应该取消所有正在运行的任务。例如 :
我们采用非保持fragment在竖屏情况下启动app。
8.我们采用非保持fragment在竖屏情况下启动app。
9.Fragment被实例化之后,调用onCreate(), onCreateView(),和createPresenter(),然后通过调用**presenter.attachView()**将**view(fragment)**附着到presenter。
10.下一步我们旋转设备屏幕,从竖屏切换到横屏。
11.此时onDestroyView() 会调用,而**onDestroyView()** 又会调用**presenter**的**detachView(true)**函数。**Presenter**取消后台任务。
12. **onSaveInstanceState(Bundle)**被调用, **ViewState**被保存到Bundle中。
13. **App**现在出于横屏状态。新的Fragment被实例化并调用onCreate(),onCreateView()和 **createPresenter()**来创建一个新的presenter例子,通过调用**presenter.attachView()**将新的view附着到新的presenter
14. **ViewState**会从**Bundle**中恢复,且view的状态也会被恢复。如果ViewState是showLoading,那么**presenter**会重新启动后台线程来加载数据。
15. 以下是获得ViewState支持的Activity的生命周期图解,如图1-6:

以下是获得ViewState支持的Fragment的生命周期图解, 如图 1-7:

##Retrofit模块
Mosby提供了 **LceRetrofitPresenter** 和 **LceCallback**。为获得LCE方法showLoading(), showContent() 和 showError()支持的Retrofit编写presenter ,几行代码就能搞定。
```java
public class MembersPresenter extends LceRetrofitPresenter>{
private GithubApigithubApi;
public MembersPresenter(GithubApi githubApi){
this.githubApi=githubApi;
}
public void loadSquareMembers(boolean pullToRefresh){
githubApi.getMembers("square",new LceCallback(pullToRefresh));
}
}
```
##Dagger模块
想在不依靠注入式的情况下写应用?Ted Mosby告诉你,这是行不通滴!Dagger是java依赖注入式框架最常用的方法,也是Android开发者们的心头好。Mosby支持Dagger1。Mosby通过一个叫做getObjectGraph()的方法提供Injector界面。通常,我们的应用模块非常广泛。要想轻松分享这一模块,我们需要把android.app.Application归入子类,使其执行Injector。之后所有的Activity和Fragment都可以通过调用getObjectGraph()来存取ObjectGraph,因为**DaggerActivity and DaggerFragment**也都是Injector。我们也可以通过重写Activity 或 Fragment中的 getObjcetGraph() ,从而调用plus(Module)以增加模块。我个人已经用到Dagger2了,它与Mosby也兼容。大家可以在Github上找到关于Dagger1 和 Dagger2的示例。点此这个链接[https://db.tt/3fVqVdAz](https://db.tt/3fVqVdAz )Dagger1示例 apk;点此这个链接[https://db.tt/z85y4fSY]( https://db.tt/z85y4fSY )Dagger2 示例 apk。
##Rx模块
**Observables**赞爆了!现在稍微潮一点的小伙儿们都用RxJava了好吗!你猜结果怎么着?RxJava确实是太酷了!所以,Mosby给大家提供一个本质上是Subscriber的MvpLceRxPresenter,它能帮我们自动处理**onNext(), onCompleted() 和 onError()并回调相应的LCE方法,比如showLoading(), shwoContent() **和 showError()。它还将 RxAndroid 附带到observerOn() Android主要 UI 线程。你可能觉得,要是用了RxJava的话就不再需要Model View Presenter了。呃,那只是你的一家之言。在我看来,把View和Model清晰地区分开来非常重要。而且我也认为其中的某些好用的功能在没有MVP的情况下不容易执行。最后,大家要是还想回到过去那个Activity和Fragment包含了上千条又臭又长的代码行时代,那么我祝你在面条式代码的地狱里过得愉快。好了,废话不多说,我介绍的方法不属于面条式代码是因为Observerables引入了一个结构齐整的工作流,把Activity或Fragment做成一个BLOB的想法已经近在咫尺了。
##测试模块
大家可能注意到这里存在着一个测试模块。这个模块用于Mosby库的内部测试。但是,它也可以为我们自己的app所用。它使用Robolectric为我们的LCE Presenter, Activities 和 Fragments提供单元测试模板。它的基本功能是查看测试中的Presenter是否正确工作:通过观察presenter时候调用showLoading(),
**showContent()** 和 **showError()**。我们还可以验证setData()中的数据。所以我们可以为Presenter和底层编写类似黑匣子的测试。Mosby的测试模块也提供了测试MvpLceFragment 或 **MvpLceActivity**的可能性。它相当于一种“精简版”的UI 测试。这些测试通过查看xml布局是否包含R.id.loadingView, R.id.contentView 和R.id.errorView之类的指定id、loadingView是否可视,在加载view时,是否是错误的view可视、content view能否处理由setData()提交的已加载数据等方面来检验Fragment或Activity是否正常工作,是否遇到crashing。它和Espresso类的UI测试并不相同。我觉得没有必要为LCE View单独写一个UI 测试。
以下是Ted Mosby库的一些测试小建议:
1. 编写传统的单元测试来测试业务逻辑层和model。
2. 使用**MvpLcePresenterTest**来测试presenter。
3.使用**MvpLceFragmentTest** 和 **MvpLceActivityTest**来测试MvpLceFragment 和 Activity。
4.如果有必要,可以使用Espresso来编写UI测试。
测试模块尚未完成。大家可以看到这个模块是测试版,因为Robolectric 3.0还没完成,而且Android gradle plugin也没用完全支持传统的单元测试。android gradle plugin
1.2应该会好得多。Robolectric 和 androids gradle plugin可以用了之后我会再写一篇关于Mosby,Dagger,Retrofit和RxJava单元测试的博客。
================================================
FILE: issue-12/readme.md
================================================
# 2015.5.31 ( 第十二期 )
| 文章名称 | 译者 |
|---------|--------|
| [自动化Android开发](自动化Android开发.md) | [tmc9031](https://github.com/tmc9031) |
| [Android进行单元测试难在哪-part4](Android进行单元测试难在哪-part4.md) | [chaossss](https://github.com/chaossss)|
| [当复仇者联盟遇上Dragger2、RxJava和Retrofit的巧妙结合](当复仇者联盟遇上Dragger2、RxJava和Retrofit的巧妙结合.md) | [Rocko](https://github.com/Rocko) |
| [Ted Mosby - 软件架构](MVP框架Mosby架构详解.md) | [Mr.Simple](https://github.com/bboyfeiyu)|
| [Android自动截屏工具](Android自动截屏工具.md) | [sundroid](https://github.com/sundroid) |
| [Android上MVP的介绍](Android上MVP的介绍.md) | [MiJack](https://github.com/MiJack) |
================================================
FILE: issue-12/当复仇者联盟遇上Dragger2、RxJava和Retrofit的巧妙结合.md
================================================
当复仇者联盟遇上Dragger2、RxJava和Retrofit的巧妙结合
---
> * 原文链接 : [When the Avengers meet Dagger2, RxJava and Retrofit in a clean way](http://saulmm.github.io/when-Thor-and-Hulk-meet-dagger2-rxjava-1/)
* 原文作者 : [Saúl M](http://saulmm.github.io/)
* [译文出自 : 开发技术前线 www.devtf.cn](http://www.devtf.cn)
* 译者 : [zhengxiaopeng](https://github.com/zhengxiaopeng)
* 校对者: [chaossss](https://github.com/chaossss)
* 状态 : 完成
最近,许多文章、框架和 android 社区中的讨论都出现关于测试和软件架构方面的内容,就像上次 [Droidcon Spain](http://es.droidcon.com/2015/speakers/) 上所说的,我们专注于做出健壮的程序而不是去开发特性功能。这些现象也意味着 Android 框架和当前 Android 社区的日渐成熟。
如果你是一名 Android 开发者,而到现在你还没听过 [Dagger 2](http://google.github.io/dagger/)、[RxJava](https://github.com/ReactiveX/RxJava)、[Retrofit](http://square.github.io/retrofit/) 这些名词的话你就错过了一些东西了,这个(文章)系列将会把一些关注点放在怎么用一种 [清晰架构](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html) 去综合使用这些框架。
我刚开始的想法是仅仅写一篇文章的,但是看到这些框架中有大量的内容所以我最终决定写一个最少 3 篇的系列文章。
一如既往,所有的代码都放在了 [Github](https://github.com/saulmm/Avengers),所有的建议、错误提交和评论都欢迎,我可能没那么多时间去回答所有问题,先说声抱歉 :)

## 依赖注入与 Dagger 2
弄懂这个框架的工作机制花费了一些时间,所以我将会根据我所学习到的内容用更加清晰的方式写出来。
[Dagger 2](http://google.github.io/dagger/)是基于 [依赖注入](http://en.wikipedia.org/wiki/Dependency_injection) 模式的。
看下下面的代码片段:
``` Java
// Thor is awesome. He has a hammer!
public class Thor extends Avenger {
private final AvengerWeapon myAmazingHammer;
public Thor (AvengerWeapon anAmazingHammer) {
myAmazingHammer = anAmazingHammer;
}
public void doAmazingThorWork () {
myAmazingHammer.hitSomeone();
}
}
```
雷神(Thor)需要一个 `复仇者武器(AvengerWeapon)` 才能正确工作,依赖注入的基本思想是,如果雷神不是通过构造器创建他自己的 `复仇者武器` 而是在内部自己创建了出来那么他就不能得到很多的优势。如果雷神自己创建出雷锤将会增加耦合度。
`复仇者武器(AvengerWeapon)` 可以是一个接口,根据我们的逻辑可以有不同的实现和注入方式。
在 Android 中,因为框架已经设计好了,我们并不总是能访问构造器,`Activity` 和 `Fragment` 就是这样的例子。
这些依赖注入器框架像 [http://google.github.io/dagger/](http://google.github.io/dagger/)、[Dagger](http://square.github.io/dagger/) 、[Guice](https://github.com/google/guice) 可以给我们带来便利。
使用 [Dagger 2](http://google.github.io/dagger/) 我们可以把之前的代码改写成这样:
``` Java
// Thor is awesome. He has a hammer!
public class Thor extends Avenger {
@Inject AvengerWeapon myAmazingHammer;
public void doAmazingThorWork () {
myAmazingHammer.hitSomeone();
}
}
```
我们没有直接访问雷神的构造方法,注入器使用了几个指令去创建了雷神的雷锤
``` Java
public class ThorHammer extends AvengerWeapon () {
@Inject public AvengerWeapon() {
initGodHammer();
}
}
```
`@Inject` 注解用于告诉 Dagger 2 构造器有用于创建雷神的雷锤。
## Dagger 2
[Dagger 2](http://google.github.io/dagger/) 由 Google 开发和维护,是 [Square](https://corner.squareup.com/) 的 [Dagger](http://square.github.io/dagger/) 项目的分支。
首先必须配置注解处理器,`android-apt` 插件就是负责这个角色,允许使用注解处理器而不将其插入到最后的 .apk 中。处理器还配置由该处理器所产生的源代码。
`build.gradle` (项目的根目录中)
``` Gradle
dependencies {
...
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
}
```
`build.gradle` (你的 android module 中)
``` Gradle
apply plugin: 'com.neenbedankt.android-apt'
dependencies {
...
apt 'com.google.dagger:dagger-compiler:2.0'
}
```
## 组件(Components)、模块(modules)和复仇者
模块负责提供依赖,组件负责注入它们(依赖)。
这是一个例子:
``` Java
@Module
public class AppModule {
private final AvengersApplication mAvengersApplication;
public AppModule(AvengersApplication avengersApplication) {
this.mAvengersApplication = avengersApplication;
}
@Provides @Singleton
AvengersApplication provideAvengersAppContext () {
return mAvengersApplication;
}
@Provides @Singleton
Repository provideDataRepository (RestRepository restRepository) {
return restRepository;
}
}
```
这个就是主模块,我们感兴趣的是它的依赖存在于程序的生命周期中,一个通用的上下文和一个取回信息的仓库。
很简单,对吧?
我们在 Dagger 2 中所说的 `@Provides` 注解,如果有需要则必须会创建其依赖。因此如果我们没有给一个特别的依赖指定一个提供者(provider),Dagger 2 将会去寻找有 `@Inject` 注解的构造方法。
组件使用模块去完成依赖注入,看看这个模块的组件:
``` Java
@Singleton @Component(modules = AppModule.class)
public interface AppComponent {
AvengersApplication app();
Repository dataRepository();
}
```
这个模块并不由任何的 activity 或者 fragment 去调用,而是通过更复杂的模块,以提供这些需要得到的依赖
``` Java
AvengersApplication app();
Repository dataRepository();
```
组件必须暴露它们的依赖给图(该模块提供的依赖关系),也即是这个模块提供的依赖关系必须对其它组件是可见的,其它的组件有把当前这个组件作为依赖,如果这些依赖关系是不可见的,Dagger 2 将不会注入这些要求的依赖。
下面是我们的依赖关系树:

``` Java
@Module
public class AvengersModule {
@Provides @Activity
List provideAvengers() {
List avengers = new ArrayList<>(6);
avengers.add(new Character(
"Iron Man", R.drawable.thumb_iron_man, 1009368));
avengers.add(new Character(
"Thor", R.drawable.thumb_thor, 1009664));
avengers.add(new Character(
"Captain America", R.drawable.thumb_cap,1009220));
avengers.add(new Character(
"Black Widow", R.drawable.thumb_nat, 1009189));
avengers.add(new Character(
"Hawkeye", R.drawable.thumb_hawkeye, 1009338));
avengers.add(new Character(
"Hulk", R.drawable.thumb_hulk, 1009351));
return avengers;
}
}
```
这个模块将会用于一个特别的 activity 的依赖注入,实际上就是负责绘画的复仇者名单:
``` Java
@Activity
@Component(
dependencies = AppComponent.class,
modules = {
AvengersModule.class,
ActivityModule.class
}
)
public interface AvengersComponent extends ActivityComponent {
void inject (AvengersListActivity activity);
List avengers();
}
```
再次,我们暴露出我们的依赖:`List` 给其它的组件,在这种情况下出现了一个新方法:`void inject (AvengersListActivity activity)` 。**在此方法被调用时**,这些依赖关系将可被消耗,将会被注入给 `AvengerListActivity` 。
## 结合所有
我们的类 `AvengersApplication`,将负责提供应用到其他组件的组件,注意,仅仅提供组件而不会用于注入依赖。
再次提醒的是 [Dagger 2](http://google.github.io/dagger/) 是在编译时生成必要的元素,如果你没有构建项目你是找不到 `DaggerAppComponent` 类的。
Dagger 2 从你的组件中生成的类的格式:`Dagger$${YourComponent}.` 。
`AvengersApplication.java`
``` Java
public class AvengersApplication extends Application {
private AppComponent mAppComponent;
@Override
public void onCreate() {
super.onCreate();
initializeInjector();
}
private void initializeInjector() {
mAppComponent = DaggerAppComponent.builder()
.appModule(new AppModule(this))
.build();
}
public AppComponent getAppComponent() {
return mAppComponent;
}
}
```
`AvengersListActivity.java`
``` Java
public class AvengersListActivity extends Activity
implements AvengersView {
@InjectView(R.id.activity_avengers_recycler)
RecyclerView mAvengersRecycler;
@InjectView(R.id.activity_avengers_toolbar)
Toolbar mAvengersToolbar;
@Inject
AvengersListPresenter mAvengersListPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_avengers_list);
ButterKnife.inject(this);
initializeToolbar();
initializeRecyclerView();
initializeDependencyInjector();
initializePresenter();
}
private void initializeDependencyInjector() {
AvengersApplication avengersApplication =
(AvengersApplication) getApplication();
DaggerAvengersComponent.builder()
.avengersModule(new AvengersModule())
.activityModule(new ActivityModule(this))
.appComponent(avengersApplication.getAppComponent())
.build().inject(this);
}
```
当 `initializeDependencyInjector()` 中执行到 `.inject(this)` 中时 [Dagger 2](http://google.github.io/dagger/) 就会开始工作并提供必要的依赖关系,请记住 [Dagger 2](http://google.github.io/dagger/) 在注入时是严格执行的,我要说的意思是组件必须是 **完全相同** 的组件类,此组件类负责调用 `inject()` 方法。
`AvengersComponent.java`
``` Java
...
public interface AvengersComponent extends ActivityComponent {
void inject (AvengersListActivity activity);
List avengers();
}
```
否则,依赖关系将不会被解决。在如下情况,presenter 与由 Dagger 2 提供的复仇者一起初始化:
``` Java
public class AvengersListPresenter implements Presenter, RecyclerClickListener {
private final List mAvengersList;
private final Context mContext;
private AvengersView mAvengersView;
private Intent mIntent;
@Inject public AvengersListPresenter (List avengers, Context context) {
mAvengersList = avengers;
mContext = context;
}
```
[Dagger 2](http://google.github.io/dagger/) 将会解决这个 presenter,因为其有 `@Inject` 注解。该构造方法的参数由 Dagger 2 解决,因为它知道怎么去构建它,这得益于这个模块中的 `@Provides` 方法。
## 总结
像 [Dagger 2](http://google.github.io/dagger/) 这样,使用好了依赖注入器,其力量是无可争辩的,想象下根据该框架提供的 API 级别你可以有不同的策略,其可能性是无止境的。
## 资源
- **Chiu-Ki Chan** - [Dagger 2 + Espresso + Mockito](http://blog.sqisland.com/2015/04/dagger-2-espresso-2-mockito.html)
- **Fernando Cejas** - [Tasting Dagger 2 on Android](http://fernandocejas.com/2015/04/11/tasting-dagger-2-on-android/)
- **Google Developers** - [Dagger 2, A new type of dependency injection](https://www.youtube.com/watch?v=oK_XtfXPkqw)
- **Mike Gouline** - [Dagger 2, Even sharper, less square](http://blog.gouline.net/2015/05/04/dagger-2-even-sharper-less-square/)
================================================
FILE: issue-12/自动化Android开发.md
================================================
自动化 Android 开发
---
> * 原文链接:[Automating Android development](https://medium.com/google-developer-experts/automating-android-development-6daca3a98396)
> * 原文作者:[Enrique López Mañas](https://medium.com/google-developer-experts/automating-android-development-6daca3a98396)
> * 译者:[tmc9031](https://github.com/tmc9031)
> * 校对者:[Mr.Simple](https://github.com/bboyfeiyu)
> * 状态:完成
我最近已经在 [DroidCon Spain](http://es.droidcon.com/2015/) 和 [DroidCon Italy](http://it.droidcon.com/2015/) 讨论过关于如何自动化传统的Android工作流。
令我惊讶的是,仍然还有很多组织缺少执行持续集成(CI)的策略。这是一个巨大的灾难!
我决定用文字表达我的思想,就是如何高效的实现持续集成(CI)。
作为一个软件攻城狮,你的目标应该是尽可能多的自动化许多工作流程。
计算机比人更高效,它们不需要吃饭睡觉,它们的任务表现没有错误,并且它们使你的生活更轻松。
请记住:*做的越多是为了了做的更少*
持续集成(CI)尽管是一个包含许多不同要点的综合领域,并且你需要把它们整合在一起。
比如,你需要讨论 Jira,你需要关心测试和分支,你需要脚本和构建。
这里有一大块我想在这个帖子中介绍的。他们的每一点都值得展开介绍,但这不是这篇文章的目的。其目的是展示给你每个基础知识,以及如何组合他们。
1. 定义一个分支策略。
2. 使用敏捷方法
3. Gradle和构建脚本
4. 测试
5. 使用CI服务器。
### 分支策略
分支是重要的。当你正在构建一个新的产品,你想建立一个协议如何工作。
人们怎么应该提交自己的特性?我们如何发布?我们如何保证不正在破坏什么东西?
要回答这些问题,你需要采用分支策略。
我正在使用一个经过轻微修改的由 [Vincent Driessen](http://nvie.com/posts/a-successful-git-branching-model/) 提出的分支策略。
让我们考虑我们应用程序的三种状态:**alpha**, **beta** 和 **release**.
* **Alpha** 的状态是你的系统正在开发。
* **Beta** 当你测试的功能已被批准和合并。
* **Release** 是当系统可交付的状态。
> (一些人喜欢叫 alpha 为 “develop” ,并且 beta 为 “stage”. 我认为希腊字母表的字母总是更cool的).
下面的图片是一个项目的第一个状态。你有一个初始的提交到了master分支。

我们的系统才刚刚开始。只有第一个提交在master分支,其他分支还是空的。
追着工作时间的进行。你需要从初始状态进入到develop过程。这将是你的1.0.1版本。

> 在这个点上,alpha的确与master相同
现在,你会在功能特性上开始工作。对于每一个特性,你会创建一个分支。
使用正确的命名是很重要的,有几种方法可以做到这一点。如果您使用的是一个问题跟踪系统像 [Jira](https://www.atlassian.com/software/jira),你可能会用一个与功能相关联的标签名(比如 FEATURE-123)。
当我提交特性时,我会包括分支名在提交信息里,并添加一个完整的描述。
*[FEATURE-123] Created a new screen that performs an action.*

注意,在每个分支的项目里都有自己的版本号。您可以使用 [git tags](http://git-scm.com/book/en/v2/Git-Basics-Tagging) 来执行版本号的控制
当一个功能已经完成,一个 pull request 就开放了,所以你的组织的其他成员就可以批准它。这是为了确保你提供高质量软件的一个关键部分。在 [Sixt](http://www.sixt.de/mobileapps/),另一成员被分配到你的代码审查,这个人会通读你全部的代码。
我们保证我们的代码是符合我们的[coding conventions](https://speakerdeck.com/kikoso/android-coding-guidelines)和我们对过程中严格的要求,典型的如在XML文件中有一个额外的空格。我们评论的命名(“函数名我不清楚”),检查我们的设计是完美的(“你的文本视图的颜色 \#DCDCDC 但设计是 \#DEDEDE”)有一个功能测试去检查该特性是覆盖了在问题跟踪里编写的验收标准的。
我们甚至进行一些哲学讨论,比如关于null和void或empty变量的意义。这听起来令人讨厌,但它是有趣的。如果是充满热情的做了这一切,到时候你就知道你的代码达到了产品需要的提交质量。
### 敏捷和迭代
你可能会在执行[SCRUM](http://en.wikipedia.org/wiki/Scrum_%28software_development%29), [Kanban](http://en.wikipedia.org/wiki/Kanban_%28development%29)或另外的敏捷方法。通常你会进行在几个星期的冲刺工作。我们认为是一个好主意,把冲刺分到两周:第一周是用来开发特性,而第二周将稳定在第一阶段创造的特性。在第二个冲刺中,我们将修复发现的漏洞,实现完美的布局或提高重构我们的代码。这项工作是在 **beta/stage** 分支完成的。
如下图所示

> 这些黄点属于bug修复和稳定性阶段
如果你遵循我们的约定,敏捷结束时你将会有一个可交付的成果。这将是一个准备发表在谷歌商店的可交付文件。此时此刻,我们的应用程序的最后版本已经合并到了master。
另一个非常重要的课题是如何创建一个热修复补丁。我们的模型试图通过使用代码检查和修复bug和产品稳定的第二周来阻止他们发生,但错误确实已经发生。这是发生在生产时,这种模式要求错误直接被修正在 **master** 分支。

你有没有意识到这个模型的标志?是的,就是那!该热修复补丁是不存在在我们的 **alpha/beta** 的分支。经过修复和稳定期后(第二周),我们的 **alpha** 分支是旧状态,错误仍然是存在的。我们需要立即合并到各分支来保证正确,从而确保每一个修复是存在所有的分支的。

很难理解?可能是读起来比实施来的难。
如果你没有一个分支策略,只是试图使用这个模型来开发一个特性。你会发现很容易工作,而且你甚至会开始定制!
### Gradle 和脚本化
现在您已经阅读分支模型,我们准备继续讨论接下来的步骤。Gradle是一个工具,将帮助我们自动完成很多事情。你可能熟悉Gradle(或其它家族成员,Maven和Ant)。Gradle是一个项目的自动化工具,当我们正在建设我们的应用程序时,可以使用执行功能和定义属性。它介绍了一种基于Groovy的领域语言,它能做到的基本上只受限于我们的想象力。
我以前写的一个 [post](http://codetalk.de/?p=112) 和一些技巧来使用工具。他们中的一些将非常有用,包括对于你的应用,但之后也有一些我已经应用的,我想在这里介绍。
#### 构建配置(BuildConfig)的力量
当我们编译Android应用程序时,*BuildConfig* 是一个自动生成的文件。这个文件,默认情况下,看起来如下:

BuildConfig 包含一个字段,叫 **DEBUG**,指示应用程序是否已经编译在调试模式。这个文件是高度可定制的,当我们工作在不同的构造类型时,这是非常便利的。
应用程序通常使用服务工具追踪其行为 [Google Analytics](http://www.google.com/analytics/ce/mws/), [Crashlytics](https://try.crashlytics.com/) 或其他平台。当我们正在开发应用时,我们可能不想影响这些指标(想象一个用户界面的测试,自动发布的每一天,跟踪您的登录屏幕?)。我们也会根据我们的构建有不同的域名(例如development.domain.com, staging.domain.com…),我们要使用自动的。但我们怎么可以做的干净利落?容易!在Gradle的buildTypes域,我们可以添加任何我们希望的新域。这些域将通过*BuildConfig* 在以后可用(这意味着,我们可以使用 BuildType.FIELD 来读取它们)。

在 [this post](http://codetalk.de/?p=112) 我展示了如何使用不同的图标和如何改变包的名称。利用这个我们可以安装我们应用程序的不同版本。这能够非常方便的同时看到我们的 beta, alpha 和 release 版本。
### 保持测试
测试本身,和整个过程都有它自己的中间环节。当我们谈论测试就是我们在谈论模拟的组件,关于UI测试和集成测试,关于仪器,和所有对于Android可用的不同框架。
测试是非常重要的,因为它可以防止开发者破坏现有的东西。没有测试,当我们开发一个新的功能B时,我们可以很容易地干扰一个旧的特性A。当一个新的特征被提交,是很难手动测试整个系统的,但自动的做这些就更容易控制一个系统的稳定性。
这里有了许多不同的测试可以在移动设备上实施:只是列举几个,我们可以考虑,集成测试,功能测试,性能测试和用户界面测试。每一种都有不同的功能,它们一般是定期触发以确保新功能没有破坏或干扰系统。
为了展示一个基本的例子,如何将测试在 Jenkins 集成(以及他们如何实现生成出错时停止的功能)
我们将看到一个做UI测试的小例子 [Espresso](https://code.google.com/p/android-test-kit/wiki/Espresso) 每次都是在 Jenkins 构建测试我们的Android应用程序。
### 一个应用程序的示例
我创建了一个小示例应用程序并上传到 [GitHub](https://github.com/kikoso/Android-Testing-Espresso),所以你可以来这里看看。也有一些分支使用命名约定和 pull requests,直到现在你可以看到审查的一切解释。该应用程序是相当基本的:它有一个TextView屏幕。还有三个已在文件执行的UI测试单元
[MainActivityInstrumentationTest](https://github.com/kikoso/Android-Testing-Espresso/blob/master/src/androidTest/java/com/dropsport/espressoreadyproject/tests/MainActivityInstrumentationTest.java):
1. 检查在屏幕上存在一个TextView。
2. 检查TextView包含文本“Hello World!”
3. 检查TextView包含文本“What a label!”
最后的两个试验是互斥的(这意味着,无论是一个或另一个是成功的,但不能两者同时成立)。我们通过以下命令对应用进行测试:
./gradlew clean connectedCheck.
如果你检出了代码,你可以自己试试注释功能 *testFalseLabel*。这将使测试失败。
### 把一切都集成在 Jenkins
现在,我们已经检查了一些事情,让我们看看他们如何适配 Jenkins。如果你没有安装它,你可以从网站下载 [last version](https://jenkins-ci.org/)。
我们没有提到一些东西,但这里也有分支策略。
有许多不同的方法,它们都具有各自的优点和缺点:
1. 你可以在分支构建前触发测试。
2. 你可以晚上或每日构建,不要阻止构建,但如果失败仍然要发出通知。
在本教程中我选择了第一种方法,也是为了显示 Jenkins 的一大特征:jobs 之间的依赖关系。让我们创造三个 jobs:**Job Beta**, **Job Alpha** and **Job Tests**。
1. **Job Alpha** 将构建 alpha 分支 (通过 ./gradlew clean assembleAlpha)
2. **Job Beta** 将做同样的工作在 beta 分支上(通过 ./gradlew clean assemblebeta)。这是每一次有分支合并到 beta 分支上就会执行的
3. **Job Tests** 每次有分支合并到 alpha 分支时都将触发。如果它成功了,它会引发 **Job Alpha**。

Jenkins 是一个基于大量的插件的平台。许多公司正在为他们的产品不断地发布插件,他们将集成在 Jenkins,我们可以很容易地与其他平台连接。
让我们看看在 Jenkins 的一些选项
#### 依赖
使用依赖 Jenkins 可以互连项目。也许我们要连接测试 jobs 和基于试验结果来控制启动。或许我们在实际构建应用之前,部分逻辑首先存在需要编译的lib库里。
#### 通知
Jenkins 可以通知一个人或一个工作组或构建错误。通知一般是电子邮件,但也有插件可以通过IM系统发送消息,如 [Skype](https://wiki.jenkins-ci.org/display/JENKINS/Skype+Plugin) 或者 [SMS](https://wiki.jenkins-ci.org/display/JENKINS/SMS+Notification)(最新版当你有重要的测试失败时可以很方便的通知)。
#### 交付
你可能知道,在这一点上 [HockeyApp](http://hockeyapp.net/) 或另一个 [delivery platforms](http://alternativeto.net/software/hockeyapp/)。他们基本上可以存储二进制文件,创建组,并当应用程序被上传时通知他们。想象看测试者自动在他/她的设备上接收每次他们被创造的文件,和产品所有者被通知有新的beta版的情景。这里有一个Jenkins 插件 [HockeyApp plugin](https://wiki.jenkins-ci.org/display/JENKINS/HockeyApp+Plugin) 能够上传二进制文件到 Hockey(甚至通知成员,或作为你最近提交的 release notes 使用)。

我还是喜欢保持手动发布产品的步骤,这可能是由于在出版过程中不经过人工控制的一种担心。但是,确实有一个 [plugin](https://wiki.jenkins-ci.org/display/JENKINS/Google+Play+Android+Publisher+Plugin) 用来直接发步到 Google Play。
### 结论
在 *building*, *testing*, *delivering* 和 *publishing* 实现自动化,主要是在一个团队工作中选择正确的决策。当这个决定是明确的,我们才可以继续去技术上实现。
有一件事情是肯定的:错误会由于以往人们的行动而大幅度减少,并结合强大的测试覆盖率,我们的软件质量将大大提高。这里我借用同事的座右铭 [Cyril Mottier](https://developers.google.com/experts/people/cyril-mottier):
> 致精而大
这是你职业生涯中的一个重要时刻!当你想在工作中保证 **质量** 而努力,而不是 **数量** 的多少。我了解这件事,第一步是尽你所能的实现自动化。事实上,我可以用以前的口号改述为另一句话,我将在我的日常生活里:
> 自动化的更多,所以你被动化的更少
祝:快乐编程!

================================================
FILE: issue-13/Android进行单元测试难在哪-终.md
================================================
Android 进行单元测试难在哪-终
---
> * 原文链接 : [WHAT I’VE LEARNED FROM TRYING TO MAKE AN ANDROID APP UNIT TESTABLE](http://www.philosophicalhacker.com/2015/05/22/what-ive-learned-from-trying-to-make-an-android-app-unit-testable/)
* 原文作者 : [Matthew Dupree](http://philosophicalhacker.com/)
* [译文出自 : 开发技术前线 www.devtf.cn](http://www.devtf.cn)
* 译者 : [chaossss](https://github.com/chaossss)
* 校对者: [Mr.Simple](https://github.com/bboyfeiyu)
* 状态 : 完成
在前面的博文中,我给大家介绍并展示了要怎么使用 Square 大法架构 Android 应用,事实上,Square 开发新的 Android 应用架构本意只是增强应用的可测试性。正如我在[Android 进行单元测试难在哪-序](https://github.com/bboyfeiyu/android-tech-frontier/blob/master/issue-8/Android%20%E8%BF%9B%E8%A1%8C%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E9%9A%BE%E5%9C%A8%E5%93%AA-%E5%BA%8F.md)中所说,要在 Android 中进行单元测试在大多数情况下都很费时,尝试一些奇技淫巧或者第三方库情况可能会好一些。为了实现高效且不依赖第三方库的测试单元,Square 大法应运而生。
此外,我们在[ Android 进行单元测试难在哪-part1](https://github.com/bboyfeiyu/android-tech-frontier/blob/master/issue-9/Android%E8%BF%9B%E8%A1%8C%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E9%9A%BE%E5%9C%A8%E5%93%AA-part1.md)和[ Android 进行单元测试难在哪-part3](https://github.com/bboyfeiyu/android-tech-frontier/blob/master/issue-11/Android%E8%BF%9B%E8%A1%8C%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E9%9A%BE%E5%9C%A8%E5%93%AA-part3.md)两篇博文就这个问题进行了研究,最后成功地通过 Square 大法为 Android 应用实现了测试单元,这篇文章则是对 Square 大法进行评估,我将从下面三个方面进行:
1. 为 Android 应用实现高效的测试单元并不需要移除所有对 Android SDK 的编译时依赖。(事实上这样做有些不切实际。)
2. 加入我们重新设计 Square 大法,使 Square 大法的使用不需要移除所有对 Android SDK 的编译时依赖,尝试将 Square 大法运营到项目中唯一会发生的问题是:实现一大堆模板代码。幸运的是,许多诸如此类的模板代码都可以通过 Android Studio 自动生成。
3. 依赖注入确实是让 Square 大法成为应用可测试性治病良方的好办法。
##移除所有对 Android SDK 的编译时依赖既不必要也不切合实际
完成这个系列博文的最初愿望就是通过让 Android 应用栈变成下图那样,增强应用的可测试性:

接下来我将告诉大家,这个想法一直误导着我们。要使应用的可测试性增强,我们还需要利用依赖注入的其它特性,而不仅仅是用它将业务逻辑代码和 Android SDK 解耦。最初的原因是对象的 Android 依赖可以通过类似 Mockito 的东西模拟出来,而且如果我们无法单独通过 Mocktio 完全控制测试单元的预测试状态的话,我们还可以用具有 mock 实现的接口代替这些 Android 依赖。而这也正是我们在[ Android 进行单元测试难在哪-part4](https://github.com/bboyfeiyu/android-tech-frontier/blob/master/issue-12/Android%E8%BF%9B%E8%A1%8C%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E9%9A%BE%E5%9C%A8%E5%93%AA-part4.md)中重构 SessionRepositoryManager 的办法。
移除依赖不仅是不必要的,将它完全与 Android SDK 解耦也是不切实际的,问题在于:当你尝试完成这个目标时,不妨回顾你已经完成的工作,你会很明显地感觉这是不必要的,所以我只打算在此简要地陈述个中因由。
要移除移除应用中所有对 Android SDK 的编译时依赖可能会导致下面的问题:
1. 应用必须为此定义许多方法和类。
2. 添加的接口和已有的 Android 接口几乎一样。
3. 由于有些情况下,对象需要被注入;有些情况下,对象不需要被注入,带有依赖的类构造器会因此变得很臃肿。
虽然 Square 大法存在一些问题,但这并不影响我在之前的博文中有关 Square 的讲解,Square 的这些特性依旧是实用,值得尝试的。由于 Android SDK 供予我们使用的应用组件类都没有被依赖注入,我们要在 Android 应用中进行单元测试确实很困难,但有了 Square 大法后,只要我们将业务逻辑交给被依赖注入的纯 Java 对象,就能轻易地对 Android 应用进行单元测试。尽管 Square 大法减小了移除 Android SDK 依赖的需求,但 Square 大法仍然是增强应用可测试性的屠龙宝刀。但我个人还是希望对 Square 大法进行优化,使得 Square 大法能无视这个需求,换句话说,我希望能够找到一种不需要我们移除所有对 Android SDK 依赖的架构方法,
##乏味的模板代码是阻碍应用变得可测试的绊脚石
如果我们对 Square 大法进行优化,使其不再要求我们移除对 Android SDK的依赖,Square 大法看起来真的是天下第一的神功了。委以业务逻辑的纯 Java 对象将被应用组件类引用,使它们能够访问所有组件类内的属性和回调。因此,将业务逻辑转移到进行了依赖注入的纯 Java 对象,不应该将那些拥有履行自身职责的数据的对象排除在外。
如果这是正确的,那么唯一阻止我们使用 Square 大法的就是实现乏味的模板代码。幸运的是,Android Studio 为我们提供了过渡到 Square 大法的重构选项——the Extract Delegate option。利用这个选项,可以自动地将类的方法和实例变量转移到一个委托类中,并让原始类通过调用委托类处理逻辑,而不用依赖类自身的方法。
这个[视频](www.youtube.com/embed/N0F7w4wEnQ8)向我们展示了如何利用 `the Extract Delegate option` 完成重构 SessionDetailActivity 的 onStop() 并使之能够进行单元测试必要的操作。我曾在之前的博文中给大家解释过进行这样的重构为什么是必要的,很显然,手动操作无法涵盖所有情况,而且你需要为此不断重复实现代码语句块中分离 Activity View 和数据的方法,这样重复而又没有意义的工作无异于浪费生命,因此这个选项真的非常实用。
##依赖注入是 Square 大法的精髓
[Android 进行单元测试难在哪-part3](https://github.com/bboyfeiyu/android-tech-frontier/blob/master/issue-11/Android%E8%BF%9B%E8%A1%8C%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E9%9A%BE%E5%9C%A8%E5%93%AA-part3.md)的秘密在于:**依赖注入**。
— Chris Arriola (@arriolachris) [May 15, 2015](https://twitter.com/arriolachris/status/599232312492982273)
Square 大法之所以能解决 Android 的单元测试问题,是因为它允许我们对持有业务逻辑的类进行真正的依赖注入,我之所以强调 Square 允许我们进行的是**真正的**依赖注入,是因为依赖注入这个概念在 Dagger 库中也被提到过,然而,Dagger 并不能真正简化 Android 应用进行单元测试的代码。
这是因为 Dagger 就像它介绍的那样,确实是一个 Service 定位库,正是如此,Dagger 强迫我们实现一个模块,使之为我们想要进行单元测试的对象提供 mock 依赖。为了能利用这些模块,我们还要确保由这些 mock 提供者模块构建的对象图与我们尝试进行单元测试的对象使用的对象图相同。
而正是这样的操作使得 Dagger 的依赖注入不如 Square 大法中真正的依赖注入那样简便,解释为什么 Dagger 不能简化对 Android 应用进行单元测试的过程完全可以写一篇博文,所以现在,我唯一能做的就是先指出这个问题,如果我们理解[ Martin Fowler 对依赖注入的定义](http://martinfowler.com/articles/injection.html#InversionOfControl)(这篇博文确实值得一读,因为他创建了一个新的术语),就会发现 Dagger 确实只是一个 Service 定位库,而且 Google 官方有关测试的博客也有[一篇博文](http://martinfowler.com/articles/injection.html#InversionOfControl)对此作出解释。
##结论
如果想让 Android 应用可以进行单元测试,那就用 Square 大法吧,当然了,如果有其他解决办法的话我也会支持的。在这里友情提示一下哈:我只是列出我了解的几种办法,我可没有说 Square 大法天下第一无人可敌,事实上增强应用可测试性应该还会有其他办法的。
这篇博文的发布也预示着这系列博文走向终结啦,我非常感谢每一个关注本系列博文的 Android 开发者的支持,感谢每一个人对我的鼓励、转发以及大家在社交媒体上对我的褒奖,我感恩这一切。这些积极的反馈使我认识到讨论和思考对 Android 应用进行测试的重要性,正是完成这个系列博文给我带来的启示使我决定:我要在未来花费更多的时间在博客中研究 Android 测试方面的知识。我会在每个周五更新博客,希望能学习更多有关测试的知识,和大家一起进步。
================================================
FILE: issue-13/Square:从今天开始抛弃Fragment吧!.md
================================================
Square:从今天开始抛弃Fragment吧!
---
> * 原文链接 : [Advocating Against Android Fragments](https://corner.squareup.com/2014/10/advocating-against-android-fragments.html)
* 原文作者 : [Pierre-Yves Ricau](http://twitter.com/Piwai)
* [译文出自 : 开发技术前线 www.devtf.cn](http://www.devtf.cn)
* 译者 : [chaossss](https://github.com/chaossss)
* 校对者: [Belial](www.belial.me)
* 状态 : 完成
最近我在 Droidcon Paris 上进行了[一个技术相关的演讲](http://fr.droidcon.com/2014/agenda/detail?title=D%C3%A9fragmentez+vos+apps+avec+Mortar+%21),我在这次演讲中给大家展示了 Square 使用 Fragment 进行开发时遇到的种种问题,以及其他 Android 开发者是怎么避免在项目中使用 Fragment 的。
在 2011 年那会,由于下面的原因我们决定使用 Fragment:
- 在那会,虽然我们很想让应用能在平板设备上被使用,但我们确实没能为平板提供平台支持。而 Fragment 能帮助我们完成这项愿望,建立响应式 UI 界面。
- Fragment 是视图控制器,它们能够将一大块耦合严重的业务逻辑模块解耦,并使得解耦后的业务逻辑能够被测试。
- Fragment 的 API 能够进行回退栈管理(例如,它能反射某个 Activity 内 Activity 栈的具体操作)
- 因为 Fragment 处于视图层的顶层,而为 View 设置动画并不麻烦,使得 Fragment 为设置页面切换的过渡效果提供了更好的支持。
- Google 建议我们使用 Fragment,而我们作为开发者都想让自己的代码符合标准。
在 2011年之后,我们在为 Square 进行开发的过程中发现了比使用 Fragment 更好的方法。
#关于 Fragment 你不知道的事
##The lolcycle
在 Android 中,Context 就像一个[上帝对象](http://en.wikipedia.org/wiki/God_object),因为在 Context 类中涵盖了太多 Android 系统的信息和相关的操作,使得 Context 在 Android 系统中相当于一个全知全能的上帝,而 Activity 就是为 Context 添加了生命周期的子类。不过让上帝具有生命周期还是有些讽刺的。虽然 Fragment 不是上帝对象,但 Fragment 为了能够完成 Activity 中能完成的各种操作,使 Fragment 自身的生命周期变得异常复杂。
Steve Pomeroy 做了一张[ Fragment 的完整生命周期图](https://github.com/xxv/android-lifecycle),我相信任谁看到这张图都不会好受:

这张图由 Steve Pomeroy 完成,图中移除了 Activity 的生命周期,分享这张图需要获得 [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) 许可。
整个 Fragment 的生命周期让你很头疼要怎样使用这些回调方法,它们是同步调用的呢,还是只是一次性全部调用呢,还是其它情况……?
##难于调试
当你的应用出现 Bug,你得用调试工具一步一步地执行代码才能知道到底发生了什么,虽说一般情况下这样做 Bug 都能解决,但如果你在调试的时候发现 Bug 和 FragmentManagerImpl 类存在某种联系,那么我可要好好恭喜你即将中大奖了!
因为要跟踪 FragmentManagerImpl 类内代码的执行顺序,并进行调试是很困难的,这也使得修复应用中相关的 Bug 也变得异常困难:
```java
switch (f.mState) {
case Fragment.INITIALIZING:
if (f.mSavedFragmentState != null) {
f.mSavedViewState = f.mSavedFragmentState.getSparseParcelableArray(
FragmentManagerImpl.VIEW_STATE_TAG);
f.mTarget = getFragment(f.mSavedFragmentState,
FragmentManagerImpl.TARGET_STATE_TAG);
if (f.mTarget != null) {
f.mTargetRequestCode = f.mSavedFragmentState.getInt(
FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, 0);
}
f.mUserVisibleHint = f.mSavedFragmentState.getBoolean(
FragmentManagerImpl.USER_VISIBLE_HINT_TAG, true);
if (!f.mUserVisibleHint) {
f.mDeferStart = true;
if (newState > Fragment.STOPPED) {
newState = Fragment.STOPPED;
}
}
}
// ...
}
```
如果你曾经需要解决应用旋转后产生一个与旋转前 UI 相同(方向发生变化)的独立的 Fragment 的需求,我想你应该懂我在说什么。(别给我提嵌套使用的 Fragment!)
我想下面这张图很好地诠释了这类代码给程序员带来的伤害(由于版权问题我得放出这张图的出处哈:[this cartoon](http://www.osnews.com/story/19266/WTFs_m)):

在多年的深度分析中我得出结论:操蛋程度/调试耗费的时间 = 2^m,m 为 Fragment 的个数。
##Fragment 是视图控制器?想太多
因为 Fragment 需要创建、绑定和配置 View,它们包含了许多与 View 关联的结点,这就意味着 View 类代码中的业务逻辑并没有真正地被解耦,正是这个原因使得我们要为 Fragment 实现测试单元将会变得很困难。
##Fragment transactions
Fragment 的 transaction 允许你执行一系列的 Fragment 操作,但不幸的是,提交 transaction 是异步操作,并且在 UI 线程的 Handler 队列的队尾被提交。这会在接收多个点击事件或配置发生改变时让你的 App 处在未知的状态。
```java
class BackStackRecord extends FragmentTransaction {
int commitInternal(boolean allowStateLoss) {
if (mCommitted)
throw new IllegalStateException("commit already called");
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);
} else {
mIndex = -1;
}
mManager.enqueueAction(this, allowStateLoss);
return mIndex;
}
}
```
##创建 Fragment 可能带来的问题
Fragment 的实例能够通过 Fragment Manager 创建,例如下面的代码看起来没有什么问题:
```java
DialogFragment dialogFragment = new DialogFragment() {
@Override public Dialog onCreateDialog(Bundle savedInstanceState) { ... }
};
dialogFragment.show(fragmentManager, tag);
```
然而,当我们需要存储 Activity 实例的状态时,Fragment Manager 可能会通过反射机制重新创建该 Fragment 的实例,又因为这是一个匿名内部类,该类有一个隐藏的构造器的参数正是外部类的引用,如果大家有看过[这篇博文](http://blog.csdn.net/u012403246/article/details/45666369)的话就会知道,拥有外部引用可能会带来内存泄漏的问题。
```java
android.support.v4.app.Fragment$InstantiationException:
Unable to instantiate fragment com.squareup.MyActivity$1:
make sure class name exists, is public, and has an empty
constructor that is public
```
##Fragment 教给我们的思想
尽管 Fragment 有着上面提到的缺点,但也是 Fragment 教给我们许多代码架构的思想:
- 独立的 Activity 接口:实际上我们并不需要为每一个页面创建一个 Activity,我们大可以将应用切分成许多解耦的视图组件,按照我们的实际需求把它们组装成我们想要的界面。这样做也能简化生命周期和动画设置,因为我们还能将视图组件切分为 view 组件和控制器组件。
- 回退栈不是 Activity 的特有概念,也就意味着你能在 Activity 内部实现回退栈。
- 不需要添加新的 API,我们需要的只是 Activity,View 和 LayoutInflater。
#响应式 UI:Fragment VS Custom View
##Fragment
我们不妨先来看看一个 Fragment 的[范例](http://developer.android.com/shareables/training/FragmentBasics.zip),界面中显示了一个 list。
HeadlinesFragment 就是显示 List 的简单 Fragment:
```java
public class HeadlinesFragment extends ListFragment {
OnHeadlineSelectedListener mCallback;
public interface OnHeadlineSelectedListener {
void onArticleSelected(int position);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setListAdapter(
new ArrayAdapter(getActivity(),
R.layout.fragment_list,
Ipsum.Headlines));
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mCallback = (OnHeadlineSelectedListener) activity;
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
mCallback.onArticleSelected(position);
getListView().setItemChecked(position, true);
}
}
```
现在有趣的事情来了:ListFragmentActivity 必须控制 list 是否处于同一个页面中。
```java
public class ListFragmentActivity extends Activity
implements HeadlinesFragment.OnHeadlineSelectedListener {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.news_articles);
if (findViewById(R.id.fragment_container) != null) {
if (savedInstanceState != null) {
return;
}
HeadlinesFragment firstFragment = new HeadlinesFragment();
firstFragment.setArguments(getIntent().getExtras());
getFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, firstFragment)
.commit();
}
}
public void onArticleSelected(int position) {
ArticleFragment articleFrag =
(ArticleFragment) getFragmentManager()
.findFragmentById(R.id.article_fragment);
if (articleFrag != null) {
articleFrag.updateArticleView(position);
} else {
ArticleFragment newFragment = new ArticleFragment();
Bundle args = new Bundle();
args.putInt(ArticleFragment.ARG_POSITION, position);
newFragment.setArguments(args);
getFragmentManager()
.beginTransaction()
.replace(R.id.fragment_container, newFragment)
.addToBackStack(null)
.commit();
}
}
}
```
##自定义 View
我们不妨重新实现一个简化版的只使用了 View 的代码
首先,我们会引入一个叫作“容器”的概念,“容器”的作用是帮助我们展示一项内容并处理后退操作
```java
public interface Container {
void showItem(String item);
boolean onBackPressed();
}
```
Acitivity 将假设始终存在容器,并且几乎不会将业务交给容器处理。
```java
public class MainActivity extends Activity {
private Container container;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
container = (Container) findViewById(R.id.container);
}
public Container getContainer() {
return container;
}
@Override public void onBackPressed() {
boolean handled = container.onBackPressed();
if (!handled) {
finish();
}
}
}
```
要显示的 List 也只是个平凡的 List。
```java
public class ItemListView extends ListView {
public ItemListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override protected void onFinishInflate() {
super.onFinishInflate();
final MyListAdapter adapter = new MyListAdapter();
setAdapter(adapter);
setOnItemClickListener(new OnItemClickListener() {
@Override public void onItemClick(AdapterView> parent, View view,
int position, long id) {
String item = adapter.getItem(position);
MainActivity activity = (MainActivity) getContext();
Container container = activity.getContainer();
container.showItem(item);
}
});
}
}
```
这样做的好处是:能够基于资源文件夹在不同的 XML 布局文件
`res/layout/main_activity.xml`
```xml
```
`res/layout-land/main_activity.xml`
```xml
```
下面是这些容器类的简单实现:
```java
public class DualPaneContainer extends LinearLayout implements Container {
private MyDetailView detailView;
public DualPaneContainer(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override protected void onFinishInflate() {
super.onFinishInflate();
detailView = (MyDetailView) getChildAt(1);
}
public boolean onBackPressed() {
return false;
}
@Override public void showItem(String item) {
detailView.setItem(item);
}
}
```
```java
public class SinglePaneContainer extends FrameLayout implements Container {
private ItemListView listView;
public SinglePaneContainer(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override protected void onFinishInflate() {
super.onFinishInflate();
listView = (ItemListView) getChildAt(0);
}
public boolean onBackPressed() {
if (!listViewAttached()) {
removeViewAt(0);
addView(listView);
return true;
}
return false;
}
@Override public void showItem(String item) {
if (listViewAttached()) {
removeViewAt(0);
View.inflate(getContext(), R.layout.detail, this);
}
MyDetailView detailView = (MyDetailView) getChildAt(0);
detailView.setItem(item);
}
private boolean listViewAttached() {
return listView.getParent() != null;
}
}
```
不难想象:将容器类抽象,并用这种的方式开发 App,不但不需要 Fragment,还能架构出容易理解的代码。
##View 和 Presenter
自定义 View 在应用中非常有用,但我们希望将业务逻辑从 View 中剥离,转交给特定的控制器处理,也就是接下来我们所说的 Presenter,引入 Presenter 能提高代码的可读性和可测试性。如果你不信的话,不妨看看重构后的 MyDetailView:
```java
public class MyDetailView extends LinearLayout {
TextView textView;
DetailPresenter presenter;
public MyDetailView(Context context, AttributeSet attrs) {
super(context, attrs);
presenter = new DetailPresenter();
}
@Override protected void onFinishInflate() {
super.onFinishInflate();
presenter.setView(this);
textView = (TextView) findViewById(R.id.text);
findViewById(R.id.button).setOnClickListener(new OnClickListener() {
@Override public void onClick(View v) {
presenter.buttonClicked();
}
});
}
public void setItem(String item) {
textView.setText(item);
}
}
```
我们来看看 Square 注册界面中编辑账户的页面吧!

Presenter 将在更高层级中操控 View:
```java
class EditDiscountPresenter {
// ...
public void saveDiscount() {
EditDiscountView view = getView();
String name = view.getName();
if (isBlank(name)) {
view.showNameRequiredWarning();
return;
}
if (isNewDiscount()) {
createNewDiscountAsync(name, view.getAmount(), view.isPercentage());
} else {
updateNewDiscountAsync(discountId, name, view.getAmount(),
view.isPercentage());
}
close();
}
}
```
大家可以看到,为这个 Presenter 实现测试单元犹如一缕春风拂面来,甚是舒心爽快呐~
```java
@Test public void cannot_save_discount_with_empty_name() {
startEditingLoadedPercentageDiscount();
when(view.getName()).thenReturn("");
presenter.saveDiscount();
verify(view).showNameRequiredWarning();
assertThat(isSavingInBackground()).isFalse();
}
```
##回退栈管理
通过异步处理来管理回退栈实在是牛刀杀鸡,大材小用了……我们只需要用一个超轻量级库——Flow,就可以达到目的。有关 Flow 的介绍 Ray Ryan 已经写过博客了,我就不在此赘述啦。
##我把 UI 相关的代码全都写在 Fragment 里了咋办呀,在线等,急!!!
别理你的 Fragment,你就一点一点地把 View 相关的代码移到自定义 View 里,然后把涉及到的业务逻辑交给能够与 View 进行交互的 Presenter,然后你就会发现 Fragment 沦为空壳,只有一些初始化自定义 View 和连接 View 和 Presenter 的操作:
```java
public class DetailFragment extends Fragment {
@Override public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.my_detail_view, container, false);
}
}
```
事实上到了这一步你已经可以抛弃 Fragment 了。
抛弃 Fragment 确实得花很大的功夫,但我们已经做到了,感谢[ Dimitris Koutsogiorgas ](https://twitter.com/dnkoutso)和[ Ray Ryan ](https://twitter.com/rjrjr)的伟大贡献!
##Dagger 和 Mortar 是什么?
Dagger & Mortar 与 Fragment 成正交关系,换句话说,两者间各自的变化不会影响对方,使用 Dagger & Mortar 既可以用 Fragment,也可以不用 Fragment。
[Dagger](http://square.github.io/dagger/) 能帮你将应用模块化为一张由解耦组件构成的图,它考虑了所有类间的连接关系并简化了抽取依赖的操作,并实现一个与此相关的单例对象。
[Mortar](https://github.com/square/mortar) 在 Dagger 的顶层进行操作,主要优势有如下两点:
- Mortar 为被注入组件提供简单的生命周期回调,使你能实现不会因旋转被销毁的单例 Presenter,不过需要注意的是,Mortar 将当前界面元素的状态储存在 Bundle 中,使数据不会随进程的结束而被清除。
- Mortar 为你管理 Dagger 的子图,并帮你将它们与 Activity 的生命周期关联在一起,这种功能让你能有效地实现“域”:当一个 View 被添加进来,它的 Presenter 和依赖都会作为子图被创建;当 View 被移除,你能轻易地销毁“域”,并让垃圾回收机制去完成它的工作。
##结论
我们曾为 Fragment 的诞生满心欢喜,幻想着 Fragment 能为我们带来种种便利,然而这一切不过是场虚空大梦,我们最后发现骑着白马的 Fragment 既不是王子也不是唐僧,只不过是人品爆发捡了只白马的乞丐罢了:
- 我们遇到的大多数难以解决的 Bug 都与 Fragment 的生命周期有关。
- 我们只需要 View 创建响应式 UI,实现回退栈以及屏幕事件的处理,不用 Fragment 也能满足实际开发的需求。
================================================
FILE: issue-13/readme.md
================================================
# 2015.6.5 ( 第十三期 )
| 文章名称 | 译者 |
|---------|--------|
| [Square:从今天开始抛弃Fragment吧!](Square:从今天开始抛弃Fragment吧!.md) | [chaossss](https://github.com/chaossss) |
| [Android进行单元测试难在哪-终](Android进行单元测试难在哪-终.md) | [chaossss](https://github.com/chaossss)|
| [优化android-studio编译效率的方法](优化android-studio编译效率的方法.md) | [FTExplore](https://github.com/FTExplore) |
| [创建 RecyclerView LayoutManager – Part 2](创建-RecyclerView-LayoutManager-Part-2.md) | [tiiime](https://github.com/tiiime)|
| [创建-RecyclerView-LayoutManager-Part-3](创建-RecyclerView-LayoutManager-Part-3.md) | [tiiime](https://github.com/tiiime) |
| [创建-RecyclerView-LayoutManager-Redux](创建-RecyclerView-LayoutManager-Redux.md) | [Mr.Simple](https://github.com/bboyfeiyu) |
================================================
FILE: issue-13/优化android-studio编译效率的方法.md
================================================
优化android studio编译效率的方法
---
> * 原文链接 : [Boosting the performance for Gradle in your Android projects](https://medium.com/@erikhellman/boosting-the-performance-for-gradle-in-your-android-projects-6d5f9e4580b6)
* 原文作者 : [Erik Hellman](https://medium.com/@erikhellman)
* [译文出自 : 开发技术前线 www.devtf.cn](http://www.devtf.cn)
* 译者 : [FTExplore](https://github.com/FTExplore)
## 引言
如果你之前用eclipse开发过Android app的化,转到android studio的第一反应也许就是:"编译速度有点慢". 表现的最明显的一点就是,当我使用eclipse开发的时候,选中了auto building.这个时候
我更改了几个字符,eclipse会速度非常快的编译出一个新的apk. 而android studio使用gradle编译,每次编译,即便是更改的代码量很少,也会按照预先设置的task的顺序,依次走完编译的各项流程.所以
这点就让人很痛苦. 然而问题总还是要被解决的,作者曾经亲眼看到过使用android studio仅仅用了2.5秒就编译完毕(在代码更改很少的情况下). 现在把如何优化gradle编译速度的方法记录在此,希望可以
帮助到广大的同行们.
## 准备工作
gradle现在最新的版本是2.4, 相比较之前的版本, 在编译效率上面有了一个非常大的提高,为了确保你的android项目使用的是最新版的gradle版本,有两种方法可以使用,下面依次进行介绍
### 1、在build.gradle中进行设置
在你的项目gradle文件内(不是app里面的gradle文件), 添加一个task, 代码如下:
```java
task wrapper(type: Wrapper) {
gradleVersion = '2.4'
}
```
然后打开terminal, 输入./gradlew wrapper, 然后gradle就会自动去下载2.4版本,这也是官方推荐的手动设置gradle的方法(http://gradle.org/docs/current/userguide/gradle_wrapper.html)
### 2、使用android studio对gradle版本进行设置
这种方法需要你去手动去gradle官网下载一个zip包,解压缩后,打开android studio 设置界面的Project Structure. 然后手动添加你解压缩后的gradle的磁盘路径即可,可以参考如下的图片

有一点需要注意的是,这种设置方法仅适用于在你的项目中使用gradle wrapper进行编译打包的操作(就是android studio默认需要的东东).如果你想使用gradle做其他的事情,请出门左转,去gradle官网(http://gradle.org)
## 守护进程,并行编译
通过以上步骤,我们设置好了android studio使用最新的gradle版本,下一步就是正式开启优化之路了. 我们需要将gradle作为守护进程一直在后台运行,这样当我们需要编译的时候,gradle就会立即跑过来然后
吭哧吭哧的开始干活.除了设置gradle一直开启之外,当你的工作空间存在多个project的时候,还需要设置gradle对这些projects并行编译,而不是单线的依次进行编译操作.
说了那么多, 那么怎么设置守护进程和并行编译呢?其实非常简单,gradle本身已经有了相关的配置选项,在你电脑的GRADLE_HOME这个环境变量所指的那个文件夹内,有一个`.gradle/gradle.properties`文件.
在这个文件里,放入下面两句话就OK了:
```
org.gradle.daemon=true
org.gradle.parallel=true
```
有一个地方需要注意的是,android studio 本身在编译的时候,已经是使用守护进程中的gradle了,那么这里加上了org.gradle.daemon=true就是保证了你在使用命令行编译apk的时候也是使用的守护进程.
你也可以将上述的配置文件放到你project中的根目录下,以绝对确保在任何情况下,这个project都会使用守护进程进行编译.不过有些特殊的情况下也许你应该注意守护进程的使用,具体的细节参考http://gradle.org/docs/current/userguide/gradle_daemon.html#when_should_i_not_use_the_gradle_daemon
在使用并行编译的时候必须要注意的就是,你的各个project之间不要有依赖关系,否则的话,很可能因为你的Project A 依赖Project B, 而Project B还没有编译出来的时候,gradle就开始编译Project A 了.最终
导致编译失败.具体可以参考http://gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects。
还有一些额外的gradle设置也许会引起你的兴趣,例如你想增加堆内存的空间,或者指定使用哪个jvm虚拟机等等(代码如下)
org.gradle.jvmargs=-Xmx768m
org.gradle.java.home=/path/to/jvm
如果你想详细的了解gradle的配置,请猛戳http://gradle.org/docs/current/userguide/userguide_single.html#sec:gradle_configuration_properties
## 一个实验性的功能
最后一个要介绍的是incremental dexing, 这个功能目前还在试验阶段,android studio默认是关闭的, 作者个人是非常推荐的,程序员就是爱折腾啊.
开启incremental dexing也是非常简单的,就是在app级别的buid.gradle文件中加入下面的代码:
```
dexOptions {
incremental true
}
```
感性您的阅读,希望这边文章可以对您有所帮助. 如果您有好的建议或者意见请联系我
================================================
FILE: issue-13/创建-RecyclerView-LayoutManager-Part-2.md
================================================
创建 RecyclerView LayoutManager – Part 2
---
> * 原文链接 : [Building a RecyclerView LayoutManager – Part 2][part2]
> * 原文作者 : [Dave Smith][author]
* [译文出自 : 开发技术前线 www.devtf.cn](http://www.devtf.cn)
* 译者 : [tiiime](https://github.com/tiiime)
* 校对者:[chaossss](https://github.com/chaossss)
* 状态 : 完成
# 创建 a RecyclerView LayoutManager – Part 2
>本文是这个系列中的 Part 2,这里是 [Part 1][part1] 和 [Part 3][part3] 的链接。
上次我们讲了创建一个 RecyclerView LayoutManager 的核心步骤。接下来,
我们会介绍如何给普通基于适配器的 View 加入一些附加特性。
>友情提醒:示例中的代码在这里 [Github][sample-github]
---
#Item Decorations 支持
RecyclerView 有一个很好的特性 `RecyclerView.ItemDecoration`,它可以给
子视图添加自定义样式,还可以在不修改子视图布局参数的情况下插入
布局属性(margins)。后者就是 LayoutManager 必须提供的约束子视图布局方式。
---
>[RecyclerPlayground ][sample-github] 里有几个 decorator 用来介绍它们的实现方式。
LayoutManager 中提供了一些辅助方法操作 decorations ,不需要我们自己实现:
- 用`getDecoratedLeft()`代替`child.getLeft()`获取子视图的 left 边缘。
- 用`getDecoratedTop()`代替`getTop()`获取子视图的 top 边缘。
- 用`getDecoratedRight()`代替`getRight()`获取子视图的 right 边缘。
- 用`getDecoratedBottom()`代替`getBottom()`获取子视图的 bottom 边缘。
- 使用 `measureChild() ` 或 `measureChildWithMargins()` 代替`child.measure()`
测量来自 Recycler 的新视图。
- 使用`layoutDecorated() `代替 `child.layout()` 布局来自 Recycler 的新视图。
- 使用 `getDecoratedMeasuredWidth() `或 `getDecoratedMeasuredHeight()`
代替 `child.getMeasuredWidth()`或` child.getMeasuredHeight()`获取
子视图的测量数据。
只要你使用了正确的方法去获取视图的属性和测量数据,RecyclerView 会自己搞定细节部分的处理。
---
#数据集改变
当使用 `notifyDataSetChanged()`触发 `RecyclerView.Adapter` 的更新操作时,
LayoutManager 负责更新布局中的视图。这时,`onLayoutChildren()`会被再次调用。
实现这个功能需要我们调整代码,判断出当前状态是生成一个新的视图 还是 adapter
更新期间的视图改变。下面是`FixedGridLayoutManager`中的填充方法的完整实现:
```java
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//We have nothing to show for an empty data set but clear any existing views
if (getItemCount() == 0) {
detachAndScrapAttachedViews(recycler);
return;
}
//...on empty layout, update child size measurements
if (getChildCount() == 0) {
//Scrap measure one child
View scrap = recycler.getViewForPosition(0);
addView(scrap);
measureChildWithMargins(scrap, 0, 0);
/*
* We make some assumptions in this code based on every child
* view being the same size (i.e. a uniform grid). This allows
* us to compute the following values up front because they
* won't change.
*/
mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);
detachAndScrapView(scrap, recycler);
}
updateWindowSizing();
int childLeft;
int childTop;
if (getChildCount() == 0) { //First or empty layout
/*
* Reset the visible and scroll positions
*/
mFirstVisiblePosition = 0;
childLeft = childTop = 0;
} else if (getVisibleChildCount() > getItemCount()) {
//Data set is too small to scroll fully, just reset position
mFirstVisiblePosition = 0;
childLeft = childTop = 0;
} else { //Adapter data set changes
/*
* Keep the existing initial position, and save off
* the current scrolled offset.
*/
final View topChild = getChildAt(0);
if (mForceClearOffsets) {
childLeft = childTop = 0;
mForceClearOffsets = false;
} else {
childLeft = getDecoratedLeft(topChild);
childTop = getDecoratedTop(topChild);
}
/*
* Adjust the visible position if out of bounds in the
* new layout. This occurs when the new item count in an adapter
* is much smaller than it was before, and you are scrolled to
* a location where no items would exist.
*/
int lastVisiblePosition = positionOfIndex(getVisibleChildCount() - 1);
if (lastVisiblePosition >= getItemCount()) {
lastVisiblePosition = (getItemCount() - 1);
int lastColumn = mVisibleColumnCount - 1;
int lastRow = mVisibleRowCount - 1;
//Adjust to align the last position in the bottom-right
mFirstVisiblePosition = Math.max(
lastVisiblePosition - lastColumn - (lastRow * getTotalColumnCount()), 0);
childLeft = getHorizontalSpace() - (mDecoratedChildWidth * mVisibleColumnCount);
childTop = getVerticalSpace() - (mDecoratedChildHeight * mVisibleRowCount);
//Correct cases where shifting to the bottom-right overscrolls the top-left
// This happens on data sets too small to scroll in a direction.
if (getFirstVisibleRow() == 0) {
childTop = Math.min(childTop, 0);
}
if (getFirstVisibleColumn() == 0) {
childLeft = Math.min(childLeft, 0);
}
}
}
//Clear all attached views into the recycle bin
detachAndScrapAttachedViews(recycler);
//Fill the grid for the initial layout of views
fillGrid(DIRECTION_NONE, childLeft, childTop, recycler);
}
```
---
我们根据有没有已经被 attach 的子视图来判断当前是一个新的布局还是一个更新操作。
如果是更新,我们根据第一个可见视图的 position(通过监测视图左上角是哪个子视图)
和当前 x/y 滑动的位移这些信息去执行新的 `fillGrid()`,同时保证左上角的 item 位置不变。
---
下面是一些需要特殊处理得情况:
- 当新的数据集很小,不足以滑动时,布局会将左上角重置为 position 是 0 的item。
- 如果新的数据集很小,保持当前位置会使滚动超出边界。
我们就应该调整第一个 item 的位置,以便和右下角对齐。
---
###onAdapterChanged()
这个方法提供了另一个重置布局的场所,设置新的 adapter 会触发这个事件
(在这,`setAdapter`会被再次调用)。
这个阶段你可以安全的返回一个与之前 adapter 完全不同的视图。所以,
示例中我们移除了所有当前视图(并没有回收它们)。
```java
@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
//Completely scrap the existing layout
removeAllViews();
}
```
移除视图会触发一个新的布局过程,当 `onLayoutChildren()` 被再次调用时,
我们的代码会执行创建新视图的布局过程,因为现在没有 attched 的子视图。
---
#Scroll to Position
另一个重要的特性就是给 LayoutManager 添加滚动到特定位置的功能。
可以带有有动画效果,也可以没有,下面是对应的两个回调方法。
###scrollToPosition()
当 layout 应该立即将所给位置设为第一个可见 item 时,调用 RecyclerView 的 `scrollToPosition()`。
在一个 vertical list 里,item 应该放在顶部;horizontal list 中,通常放在左边。在我们的
网格布局中,被选中的 item 应该放在视图的左上角。
```java
@Override
public void scrollToPosition(int position) {
if (position >= getItemCount()) {
Log.e(TAG, "Cannot scroll to "+position+", item count is "+getItemCount());
return;
}
//Ignore current scroll offset, snap to top-left
mForceClearOffsets = true;
//Set requested position as first visible
mFirstVisiblePosition = position;
//Trigger a new view layout
requestLayout();
}
```
因为有一个良好的 `onLayoutChildren()` 实现,这里就可以简单的更新目标位置并触发一个
新的 fill 操作。
---
###smoothScrollToPosition()
在带有动画的情况下,我们需要使用一些稍微不同的方法。
在这方法里我们需要创建一个 `RecyclerView.SmoothScroller`实例,
然后在方法返回前请求`startSmoothScroll()`启动动画。
`RecyclerView.SmoothScroller` 是提供 API 的抽象类,含有四个方法:
---
- `onStart()`:当滑动动画开始时被触发。
- `onStop()`:当滑动动画停止时被触发。
- `onSeekTargetStep()`:当 scroller 搜索目标 view 时被重复调用,这个方法负责读取提供的
dx/dy ,然后更新应该在这两个方向移动的距离。
- 这个方法有一个`RecyclerView.SmoothScroller.Action`实例做参数。
通过向 action 的 `update()`方法传递新的 dx, dy, duration 和 `Interpolator` ,
告诉 view 在下一个阶段应该执行怎样的动画。
- **NOTE:** 如果动画耗时过长,框架会对你发出警告,
应该调整动画的步骤,尽量和框架标准的动画耗时相同。
- `onTargetFound()`:只在目标视图被 attach 后调用一次。 这是将目标视图要通过动画移动到准确位置最后的场所。
- 在内部,当 view 被 attach 时使用 LayoutManager 的 `findViewByPosition()` 方法
查找对象。如果你的 LayoutManager 可以有效匹配 view 和 position ,
可以覆写这个方法来优化性能。默认提供的实现是通过每次遍历所有子视图查找。
---
你可以自己实现一个 scroller 达到你想要的效果。不过这里我们只使用系统提供的
`LinearSmoothScroller` 就好了。只需实现一个方法`computeScrollVectorForPosition()`,
然后告诉 scroller 初始方向还有从当前位置滚动到目标位置的大概距离。
---
```java
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, final int position) {
if (position >= getItemCount()) {
Log.e(TAG, "Cannot scroll to "+position+", item count is "+getItemCount());
return;
}
/*
* LinearSmoothScroller's default behavior is to scroll the contents until
* the child is fully visible. It will snap to the top-left or bottom-right
* of the parent depending on whether the direction of travel was positive
* or negative.
*/
LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) {
/*
* LinearSmoothScroller, at a minimum, just need to know the vector
* (x/y distance) to travel in order to get from the current positioning
* to the target.
*/
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
final int rowOffset = getGlobalRowOfPosition(targetPosition)
- getGlobalRowOfPosition(mFirstVisiblePosition);
final int columnOffset = getGlobalColumnOfPosition(targetPosition)
- getGlobalColumnOfPosition(mFirstVisiblePosition);
return new PointF(columnOffset * mDecoratedChildWidth, rowOffset * mDecoratedChildHeight);
}
};
scroller.setTargetPosition(position);
startSmoothScroll(scroller);
}
```
---
在这个实现中,和现有 ListView 的行为相似,无论是 RecyclerView 的哪个方向滚动,
当视图完全可见时滚动就会停止。
---
#接下来?
我们现在已经有些起色了!事实上还有很多可以实现的功能。在[下篇文章][part3]中,我们会介绍
如何给你的 LayoutManager 提供数据集改变时的动画效果。
---
[part1]:../issue-9/创建-RecyclerView-LayoutManager-Part-1.md
[author]:http://wiresareobsolete.com/
[part2]:http://wiresareobsolete.com/2014/09/recyclerview-layoutmanager-2/
[part3]:./创建-RecyclerView-LayoutManager-Part-3.md
[sample-github]:https://github.com/devunwired/recyclerview-playground
================================================
FILE: issue-13/创建-RecyclerView-LayoutManager-Part-3.md
================================================
创建 RecyclerView LayoutManager – Part 3
---
> * 原文链接 : [Building a RecyclerView LayoutManager – Part 3][part3]
> * 原文作者 : [Dave Smith][author]
* [译文出自 : 开发技术前线 www.devtf.cn](http://www.devtf.cn)
* 译者 : [tiiime](https://github.com/tiiime)
* 校对者:[chaossss](https://github.com/chaossss)
* 状态 : 完成
# Building a RecyclerView LayoutManager – Part 3
>本文是这个系列中的 Part 3,这里是 [Part 1][part1] 和 [Part 2][part2] 的链接。
在[之前的文章][part2]里,我们讨论了怎样对数据集改变和定量滚动
提供正确的支持。接下来我们会介绍怎样给 LayoutManager 添加
合适的动画效果。
>友情提醒:示例中的代码在这里 [Github][sample-github]
---
#The Problem With Free
上次我们说到了 `notifyDataSetChanged()` ,但是使用这个方法
不会有数据改变的动画效果(1)。
RecyclerView 提供了新的 API 让我们可以通知 adapter
做出带有动画效果的改变。它们是:
- `notifyItemInserted()` 和 `notifyItemRangeInserted()`:
在给定位置/范围插入新item(s)。
- `notifyItemChanged()` 和 `notifyItemRangeChanged()`:
使给定 位置/范围 的 item(s) 无效,数据集并没有结构上的改变。
- `notifyItemRemoved()` 和 `notifyItemRangeRemoved()`:
移除给定 位置/范围 的 item(s)。
- `notifyItemMoved()`:
将数据集中的一个 item 重定位到一个新的位置。
使用这些方法你的 LayoutManager 会得到一个很简单的默认 item 动画。
这些动画是根据当前每一个 view 在改变后是否还存在于 layout 之中生成的。
新的 view 渐入,被移除的 view 淡出,其他 view 移动到新的位置。
下面是我们 grid layout 示例的动画效果:
![img][animate-01]
不过这里有个问题,一些没有被移除的 item 也淡出了。
这是因为它们在父控件 RecyclerView 的边界中不再可见。
我们想要这些 view 朝着用户期望的方向滑去,但是框架只知道
我们的代码在数据改变后没有显示出这些 item。此外,
新加入的 view 是淡入进来的,让他们从预定位置滑入界面会更好。
我们要给 LayoutManager 加点东西才能实现这些。
---
#Predictive Item Animations
下面的图片展示了我们期望的移除 item 动画效果:
![img][animate-02]
左侧一列 items 滑到右侧填补空白部分的动画很引人注意。
和这个差不多,你可以脑补出在这个位置添加一个 item 时的动画效果。
在第一篇文章里曾提到的,`onLayoutChildren()` 通常只会
在父控件 `RecyclerView` 初始化布局 或者 数据集的大小(比如 item 的数量)改变时调用一次。
Predictive Item Animations
这个特性允许我们给 view (基于数据改变产生)的过渡动画
提供更多有用的信息。想要使用这个特性,就要告诉
框架我们的 `LayoutManager` 提供了这个附加数据:
```java
@Override
public boolean supportsPredictiveItemAnimations() {
return true;
}
```
有了这个改动,`onLayoutChildren() `会在每次数据集改变后被调用两次,
一次是"预布局"(pre-layout)阶段,一次是真实布局(real layout)。
---
#在 pre-layout 阶段该做些什么
在`onLayoutChildren()`的 pre-layout 阶段,
你应该运行你的布局逻辑设置动画的**初始状态**。
这需要你在动画执行前布局所有 当前可见的 view **和** 在动画后会可见的 view
(被称为 **APPEARING** view)。Appearing views 应该被布局在
屏幕之外,用户期望它进入的位置。框架会捕获他们的位置,
籍此创建更合适的动画效果。
>我们可以使用 `RecyclerView.State.isPreLayout()` 来检测当前处于哪一阶段
在 `FixedGridLayoutManager` 示例中,我们根据数据集的改变
使用 pre-layout 决定哪些 view 被移除。被移除的 view
在 pre-layout 的 Recycler 中仍然会被返回,所以你不用担心
会出现空白位置。如果你想要判断视图是否会被移除,
可以使用`LayoutParams.isViewRemoved()` 这个方法 。
我们的示例统计了被移除 view 的数量,让我们
对有多少空间会被 appearing views 填充有一个大概的印象。
---
```java
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
…
SparseIntArray removedCache = null;
/*
* During pre-layout, we need to take note of any views that are
* being removed in order to handle predictive animations
*/
if (state.isPreLayout()) {
removedCache = new SparseIntArray(getChildCount());
for (int i=0; i < getChildCount(); i++) {
final View view = getChildAt(i);
LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (lp.isItemRemoved()) {
//Track these view removals as visible
removedCache.put(lp.getViewPosition(), REMOVE_VISIBLE);
}
}
…
}
…
//Fill the grid for the initial layout of views
fillGrid(DIRECTION_NONE, childLeft, childTop, recycler, state.isPreLayout(), removedCache);
…
}
```
>Tip:在 pre-layout 期间,RecyclerView 会尝试用 view
>在 adapter 中的位置匹配它们的"原位置"(数据改变前的位置)。
>如果你想通过 position 请求一个 view,并且希望这个位置
>是这个视图初始化时的位置。就不要在 pre-layout 和
>real-layout 期间改变它们。
示例代码中最后的变动是对`fillGrid()`进行修改,在这里给 N
个 appearing views 布局,N 是被移除的可见视图个数。
这些 view 永远是从右侧进入的,所以他们被安排在最后一列
可见 view 的后面。
```java
private void fillGrid(int direction, int emptyLeft, int emptyTop, RecyclerView.Recycler recycler,
boolean preLayout, SparseIntArray removedPositions) {
…
for (int i = 0; i < getVisibleChildCount(); i++) {
int nextPosition = positionOfIndex(i);
…
if (i % mVisibleColumnCount == (mVisibleColumnCount - 1)) {
leftOffset = startLeftOffset;
topOffset += mDecoratedChildHeight;
//During pre-layout, on each column end, apply any additional appearing views
if (preLayout) {
layoutAppearingViews(recycler, view, nextPosition, removedPositions.size(), …);
}
} else {
leftOffset += mDecoratedChildWidth;
}
}
…
}
private void layoutAppearingViews(RecyclerView.Recycler recycler, View referenceView,
int referencePosition, int extraCount, int offset) {
//Nothing to do...
if (extraCount < 1) return;
for (int extra = 1; extra <= extraCount; extra++) {
//Grab the next position after the reference
final int extraPosition = referencePosition + extra;
if (extraPosition < 0 || extraPosition >= getItemCount()) {
//Can't do anything with this
continue;
}
/*
* Obtain additional position views that we expect to appear
* as part of the animation.
*/
View appearing = recycler.getViewForPosition(extraPosition);
addView(appearing);
//Find layout delta from reference position
final int newRow = getGlobalRowOfPosition(extraPosition + offset);
final int rowDelta = newRow - getGlobalRowOfPosition(referencePosition + offset);
final int newCol = getGlobalColumnOfPosition(extraPosition + offset);
final int colDelta = newCol - getGlobalColumnOfPosition(referencePosition + offset);
layoutTempChildView(appearing, rowDelta, colDelta, referenceView);
}
}
```
在 `layoutAppearingViews()`这个方法里,每一个 appearing view
被布局到它的"全局"位置(就是它在这个网格中占据的行/列)。
虽然位置在屏幕之外,但是为框架创建滑入 view 动画的起始点提供了
必要的数据。
---
#Changes for the “Real” Layout
[part1][part1]中我们已经讨论过布局期间的基本工作,
然而要想为我们的动画提供支持还要做一些修改。
其中之一就是判断有没有 disappearing views。在我们的示例中
是通过运行一个普通的布局过程,然后检查 Recycler
的 scrap heap 之中有没有剩下的 view。
>注意:我们之所以能以这种方式使用 scrap heap 是因为
>在每一次布局过程开始前,布局逻辑总是调用了
>`detachAndScrapAttachedViews()`这个方法。
>前面说过,这是布局中你需要遵循的最佳实践。
---
Views still in scrap that aren’t considered removed are
disappearing views. We need to lay these views out in
their off-screen positions so the animation system
can slide them out of view (instead of just fading them out).
仍在 scrap 中没有被移除的视图就是 disappearing views。
我们需要把它们放置到屏幕之外的位置,以便动画系统
将它们滑出视图(用来取代淡出动画)。
---
```java
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
…
if (!state.isPreLayout() && !recycler.getScrapList().isEmpty()) {
final List scrapList = recycler.getScrapList();
final HashSet disappearingViews = new HashSet(scrapList.size());
for (RecyclerView.ViewHolder holder : scrapList) {
final View child = holder.itemView;
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.isItemRemoved()) {
disappearingViews.add(child);
}
}
for (View child : disappearingViews) {
layoutDisappearingView(child);
}
}
}
private void layoutDisappearingView(View disappearingChild) {
/*
* LayoutManager has a special method for attaching views that
* will only be around long enough to animate.
*/
addDisappearingView(disappearingChild);
//Adjust each disappearing view to its proper place
final LayoutParams lp = (LayoutParams) disappearingChild.getLayoutParams();
final int newRow = getGlobalRowOfPosition(lp.getViewPosition());
final int rowDelta = newRow - lp.row;
final int newCol = getGlobalColumnOfPosition(lp.getViewPosition());
final int colDelta = newCol - lp.column;
layoutTempChildView(disappearingChild, rowDelta, colDelta, disappearingChild);
}
```
---
>小心:布局视图(然后将它们加入container)把它们从 scrap 列表中移除。
>在开始变化前,小心处理你需要从 scrap 中获取的视图,否则你可能会
>在这个集合上出现并发修改的问题结束运行。
和之前处理 appearing views 的代码差不多,`layoutDisappearingView()`
将所有剩余 view 放在与之对应的"全局"位置作为最终布局位置。
给框架提供必要信息创建出适当方向的滑出动画。
下面的图片可以帮你理解`FixedGridLayoutManager`之中的过程:
- 黑框是 `RecyclerView` 的可视边界。
- Red View:数据集中被移除的 item。
- Green View (Appearing View):开始时没有,在 pre-layout 过程中被布局到屏幕外的item。
- Purple Views (Disappearing views):pre-layout 时期放置在他们的原始位置 ,
real-layout 时期被布局到屏幕之外的位置。
![img][animate-03]
---
#响应屏幕外的变动
你或许注意到在上一节中我们可以判断可视 views 的移除操作。
如果变化出现在可视边界之外会怎样?这取决于你的布局结构,
像这样的变化可能需要你调整布局来达到更好的动画效果。
Adapter 会将这个变化 post 给你的 LayoutManager。你可以覆写
`onItemsRemoved()`, `onItemsMoved()`, `onItemsAdded()` 或者
`onItemsChanged()` 响应 item 的这些事件,无论 item
在当前布局中是否可见。
如果被移除的范围在可视边界之外, 调用 pre-layout 之前会调用
`onItemRemoved()`。我们可以利用它收集和这个变化有关的数据,为
这个事件可能触发的 appearing view 改变提供更好的支持。
示例中,我们像之前一样收集被移除的 view,但是将它们标记成不同的类型。
---
```java
@Override
public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
mFirstChangedPosition = positionStart;
mChangedPositionCount = itemCount;
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
…
SparseIntArray removedCache = null;
/*
* During pre-layout, we need to take note of any views that are
* being removed in order to handle predictive animations
*/
if (state.isPreLayout()) {
…
//Track view removals that happened out of bounds (i.e. off-screen)
if (removedCache.size() == 0 && mChangedPositionCount > 0) {
for (int i = mFirstChangedPosition; i < (mFirstChangedPosition + mChangedPositionCount); i++) {
removedCache.put(i, REMOVE_INVISIBLE);
}
}
}
…
//Fill the grid for the initial layout of views
fillGrid(DIRECTION_NONE, childLeft, childTop, recycler, state.isPreLayout(), removedCache);
…
}
```
---
>TIP:如果被移除的 item 是可见的,这个方法在 pre-layout
>之后还会被调用。这也就是为什么
>当被移除的可见 views 出现时我们仍要从它们获取数据。
所有步骤就位,现在我们可以启动这个应用啦。可以看到左边消失的items
移到对应行的后面。右边新出现的 items 滑动进入现有的界面。
现在,新的动画中只有被移除的 item 是淡出的了。
![img][animate-04]
---
#未完待续...
我说过...这应该是这系列中的最后一篇。不过,在编写 `FixedGridLayoutManager`
动画效果的过程中又出现了些有趣的问题,并不是所有自定义的实现。
所以在下一篇文章里(这次真的是最后一篇了),我会解决这些问题。
特别感谢 [Yiğit Boyar][thanks]提供技术支持,帮助完成这篇文章。
---
1. 如果你的 adapter 使用了固定的 IDs ,可以提供足够的数据推测哪些 view 被 移除/添加/等等
框架就会尝试 给它添加动画 ↩
---
[part1]:../issue-9/创建-RecyclerView-LayoutManager-Part-1.md
[author]:http://wiresareobsolete.com/
[part2]:./创建-RecyclerView-LayoutManager-Part-2.md
[part3]:http://wiresareobsolete.com/2015/02/recyclerview-layoutmanager-3/
[sample-github]:https://github.com/devunwired/recyclerview-playground
[animate-01]:http://i.embed.ly/1/display/resize?url=http%3A%2F%2Fwiresareobsolete.com%2Fwp-content%2Fuploads%2F2015%2F02%2FDefaultRecyclerAnimationsSmall.gif&grow=true&key=92b31102528511e1a2ec4040d3dc5c07
[animate-02]:http://i.embed.ly/1/display/resize?url=http%3A%2F%2Fwiresareobsolete.com%2Fwp-content%2Fuploads%2F2015%2F02%2FRecycleConceptSmall.gif&grow=true&key=92b31102528511e1a2ec4040d3dc5c07&height=400
[animate-03]:http://i.embed.ly/1/display/resize?url=http%3A%2F%2Fwiresareobsolete.com%2Fwp-content%2Fuploads%2F2015%2F02%2FRecycleRemove.gif&grow=true&key=92b31102528511e1a2ec4040d3dc5c07&height=400
[animate-04]:http://i.embed.ly/1/display/resize?url=http%3A%2F%2Fwiresareobsolete.com%2Fwp-content%2Fuploads%2F2015%2F02%2FPredictiveRecyclerAnimationsSmall.gif&grow=true&key=92b31102528511e1a2ec4040d3dc5c07
[thanks]:https://plus.google.com/111851968937104436377/posts
================================================
FILE: issue-13/创建-RecyclerView-LayoutManager-Redux.md
================================================
创建-RecyclerView-LayoutManager-Redux
---
> * 原文链接 : [Building A RecyclerView LayoutManager – Redux](http://wiresareobsolete.com/2015/02/recyclerview-layoutmanager-redux/)
* 原文作者 : [Dave Smith](http://wiresareobsolete.com/)
* [译文出自 : 开发技术前线 www.devtf.cn](http://www.devtf.cn)
* 译者 : [Mr.Simple](https://github.com/bboyfeiyu)
* 校对者: [chaossss](https://github.com/chaossss)
* 状态 : 完成
这篇文章是RecyclerView系列文章的结尾篇。前三部分的链接在这里 :
* [第一部分](../issue-9/创建-RecyclerView-LayoutManager-Part-1.md)
* [第二部分](./创建-RecyclerView-LayoutManager-Part-2.md)
* [第三部分](./创建-RecyclerView-LayoutManager-Part-3.md)
当我在写这个系列的最后一篇文章,也就是关于predictIve animation的讨论时,我突然想到几个很有意思的点,并且很有讨论价值。这个系列文章以调查RecyclerView是否能够以简单的方式满足竖向、横向滚动的布局需求以及开发者要定制一个LayoutManager的难度有多大开始。这篇文章中我选择了一个基本的网格布局作为自定义layoutManager的示例。
下面这幅图展示了运用这个自定义LayoutManager到RecyclerView的滚动效果。

## 打破常规设计
无论你打算如何组织Adapter的位置(从左到右,上到下等等),内容视图的视角都是显示部分不连贯数据集的区域。更确切地说,在第一个和最后一个可视位置之间的Adapter区域的数据项也会在可视区域view的外面。
这是一个很重要的点,因为它与在单轴上滚动的布局大相径庭(从而导致与当前框架下提供的默认布局背道而驰)。这些标准小工具显示,数据集是在一个连接块范围内--从第一个到最后一个可视位置之间不间断。
RecyclerView LayoutManager API会假定有一个可见的数据集合,RecyclerView LayoutManager只会显示数据集合中某个范围内可见的数据项,然后产生一个像上述所展示的网格布局一样的布局效果,这稍微有那么点挑战。在predictive animation里没有什么比这部分更加明显了。为了便于扩展,我感觉在这里指出这些毛病是非常必要的。
## 假设1 : 从不可见区域移除一个数据项不会影响当前可见的View
当你考虑到一个Adapter移除一个数据项时LinearLayoutManager或者GridLayoutManager会做何反应时,这两个LayoutManager实际上都处理得非常好。如果被移除的数据项是可见的,一个可用的区域将会空置出来,此时就需要一个周边的View进行填充。这意味着周边显示的View必须被布局到这个位置以填补缺口。然而,并没有一个示例表明当一个移除操作执行时会发送一个隐藏视图的操作,唯一被隐藏的视图是那些被显式移除的。如果被移除的View在可见区域之外,那么对于可见区域的布局不会产生影响。在这个情况之下,你不会看到任何动画,它可能做出的修改是数据项被移到其他位置。
从上述的情况看,这与文章开头我们提到的一样,LayoutManager显示的是一个不连续的数据项。然而,这一可视区域的不连续本质使得数据项在屏幕外从可视区域内移除!换种说法,它们的位置处于第一个和最后一个位置之间,但是目前数据项view并不在布局中。这导致的结果就是,屏幕外发生的数据项移除可以并且会影响我们在布局动画处理时view的生成和消失。
在有机会对view的生成进行布局时,预布局是RecyclerView动画的关键阶段。RecyclerView通过其初始位置值将view返回,据此我们可以将内容布局在其初始状态。但是,当view移除与可视区域不相交时。RecyclerView就会通过其最终位置值将view返回。这样一来,在没有附加记账的情况下处理view的生成就变得困难得多。不过话说回来,难是难,也还是可以做到。
在上一篇文章中我们看到的FixedGridLayoutManager,我们不仅要解析可视view,还要听从onItemsRemoved()的回调来找到移除点,并妥当处理所有生成的view案例。RecyclerView确保我们在需要时,该回调会在预布局之前出现(屏幕外案例),但相反情况下会在预布局之后出现。RecyclerView这么做是为了避免这些事件与你的布局产生冲突--其时间点对我们来说只是一个美丽的意外。
我们还需要追踪的一个事实就是,可视移除会以一种我们意料之中的方式偏移view的位置,而屏幕外移除则不会。这也是为什么移除会被冠以不同类型的原因。上一篇文章忽略的一点表明,在屏幕外进行移除时,我们会对view生成逻辑提供一个手动偏移。。。所以当移除可视时,位置之间会相互匹配。
```java
private void fillGrid(int direction, int emptyLeft, int emptyTop, RecyclerView.Recycler recycler,
boolean preLayout, SparseIntArray removedPositions) {
…
for (int i = 0; i < getVisibleChildCount(); i++) {
int nextPosition = positionOfIndex(i);
/*
* When a removal happens out of bounds, the pre-layout positions of items
* after the removal are shifted to their final positions ahead of schedule.
* We have to track off-screen removals and shift those positions back
* so we can properly lay out all current (and appearing) views in their
* initial locations.
*/
int offsetPositionDelta = 0;
if (preLayout) {
int offsetPosition = nextPosition;
for (int offset = 0; offset < removedPositions.size(); offset++) {
//Look for off-screen removals that are less-than this
if (removedPositions.valueAt(offset) == REMOVE_INVISIBLE
&& removedPositions.keyAt(offset) < nextPosition) {
//Offset position to match
offsetPosition--;
}
}
offsetPositionDelta = nextPosition - offsetPosition;
nextPosition = offsetPosition;
}
if (nextPosition < 0 || nextPosition >= getItemCount()) {
//Item space beyond the data set, don't attempt to add a view
continue;
}
…
if (i % mVisibleColumnCount == (mVisibleColumnCount - 1)) {
leftOffset = startLeftOffset;
topOffset += mDecoratedChildHeight;
//During pre-layout, on each column end, apply any additional appearing views
if (preLayout) {
layoutAppearingViews(recycler, view, nextPosition, removedPositions.size(), offsetPositionDelta);
}
} else {
leftOffset += mDecoratedChildWidth;
}
}
…
}
```
之后offsetPositionDelta值会作为我们在预布局中使用到的行/列位置的全局偏移被传递到layoutAppearingViews()。如果不是出于附加记账要求,这一偏移完全可以不必存在。
## 假设#2:添加新的数据项只会导致同级view的消失,而不是生成。
添加了新的数据项之后,反面为真。如果在添加时数据项为可视,那么标准布局管理器会在屏幕外推开不可视的view以便腾出空间。之前没有出现过关于这种行为会同时触发一个或多个同级view滑向可视子类位置的案例。移除也是同样的道理,在可视区域外添加数据项并不会影响可视view,所以通常来说动画也不会起作用。
对于FixedGridLayoutManager或其他任何非连贯区域的布局,在可视区域内还是可视区域外进行添加并没有多大差别。两种情况中我们都需要管理可能出现的view生成和消失。而我们在移除中使用的方案在这里则不可行,因为onItemsAdded()总是在预布局之后被调用。。。这一次我们就没那么幸运地再一次能够碰到美丽的意外了。
缺少了那次回调,关于添加数据项,我们实际上在预布局时就没有多少可做的了。这样一来就变成了布局额外的view以备不时之需,而不布局一定量的额外view就会损害性能这两种情况之间的妥协。FixedGridLayoutManager在添加数据项时不支持预测view生成。
## 一切才刚刚开始
RecyclerView APIs是一个新生事物,现有案例中还有非常多的地方需要改进的。同时,它也非常复杂,想要做对并不容易。表面上看起来RecyclerView要求大家付出的努力,背后可能大家要花10倍的精力才能实现。而且这些类型越到后面可能月蛋疼。希望正在尝试这种做法的各位同仁可以视本文为一个预警,不要盲目浪费时间,但同时我们也期待这这个框架的日渐成熟。
================================================
FILE: issue-14/Android-Design-Support-Library.md
================================================
Android Design Support Library
---
> * 原文链接 : [Android Design Support Library](http://android-developers.blogspot.jp/2015/05/android-design-support-library.html)
* 原文作者 : [Android Developers Blog](http://developer.android.com/index.html)
* [译文出自 : 开发技术前线 www.devtf.cn](http://www.devtf.cn)
* 译者 : [MiJack](https://github.com/MiJack)
* 状态 : 校对完成
Android 5.0是有史以来最重要的Android 版本之一,这其中有很大部分要归功于Material design的引入,这种新的设计语言让整个Android的用户体验焕然一新。我们的详细专题是帮助你开始采用Material Design。但是我们也知道,这种设计对于开发者,尤其是那些在意向后兼容的开发者来说是一种挑战。在Android Design Support Library的帮助下,我们为所有的开发者,所有2.1以上的设备,带来了一些重要的Material design控件。你可以在这里面找到Navigation Drawer View,输入控件的悬浮标签,Floating Action Button,Snackbar,Tab以及将这些控件结合在一起的手势滚动框架。
[YouTube的介绍](https://youtu.be/32i7ot0y78U)
###Navigation View
[Navigation drawer](http://www.google.com/design/spec/patterns/navigation-drawer.html?utm_campaign=io15&utm_source=dac&utm_medium=blog)是app识别度与内部导航的关键,保持这里设计上的一致性对app的可用性至关重要,尤其是对于第一次使用的用户。 NavigationView 通过提供抽屉导航所需的框架让实现更简单,同时它还能够直接通过菜单资源文件直接生成导航元素。

把NavigationView作为DrawerLayout的内容视图来使用,布局如下:
```
```
你会注意到 NavigationView 的两个属性:app:headerLayout - 控制头部的布局, app:menu - 导航菜单的资源文件(也可以在运行时配置)。NavigationView处理好了和状态栏的关系,可以确保NavigationView在API21+设备上正确的和状态栏交互。
最简单的抽屉菜单往往是几个可点击的菜单的集合:
```
```
被点击过的item会高亮显示在抽屉菜单中,让用户知道当前是哪个菜单被选中。
你也可以在menu中使用subheader来为菜单分组:
```
-
```
你可以通过设置一个OnNavigationItemSelectedListener,使用其setNavigationItemSelectedListener()来获得元素被选中的回调事件。它为你提供可以点击的[MenuItem](http://developer.android.com/reference/android/view/MenuItem.html?utm_campaign=io15&utm_source=dac&utm_medium=blog),让你可以处理选择事件,改变复选框状态,加载新内容,关闭导航菜单,以及其他任何你想做的操作。
###Floating labels for editing text
即便是十分简单的EditText,在Material Design 中也有提升的空间。在EditText中,当你填入第一个字符后,hint就消失了。现在将它换成[TextInputLayout](http://developer.android.com/reference/android/support/design/widget/TextInputLayout.html?utm_campaign=io15&utm_source=dac&utm_medium=blog),提示信息会变成一个显示在EditText之上的floating label,这样用户就始终知道他们现在输入的是什么。

除此以外,还可以通过setError()设置当用户输入不合法时的Error提示;
###Floating Action Button
[Floating action button](http://www.google.com/design/spec/components/buttons-floating-action-button.html?utm_campaign=io15&utm_source=dac&utm_medium=blog)是一个负责显示界面基本操作的圆形按钮。Design library中的[FloatingActionButton](http://developer.android.com/reference/android/support/design/widget/FloatingActionButton.html?utm_campaign=io15&utm_source=dac&utm_medium=blog) 实现了一个默认颜色为主题中colorAccent的悬浮操作按钮。

除了一般大小的悬浮操作按钮,它还支持mini size(fabSize=”mini”)。FloatingActionButton继承自ImageView,你可以使用android:src或者ImageView的任意方法,比如[setImageDrawable()](http://developer.android.com/reference/android/widget/ImageView.html?utm_campaign=io15&utm_source=dac&utm_medium=blog#setImageDrawable(android.graphics.drawable.Drawable)来设置FloatingActionButton里面的图标。
###Snackbar
如果你需要一个轻量级、可以快速作出反馈的控件,可以试试[SnackBar](http://www.google.com/design/spec/components/snackbars-toasts.html?utm_campaign=io15&utm_source=dac&utm_medium=blog)。[Snackbar](http://www.google.com/design/spec/components/snackbars-toasts.html?utm_campaign=io15&utm_source=dac&utm_medium=blog)显示在屏幕的底部,包含了文字信息与一个可选的操作按钮。在指定时间结束之后自动消失。另外,用户还可以滑动删除它们。
[example video](http://material-design.storage.googleapis.com/publish/material_v_3/material_ext_publish/0B6Okdz75tqQsLVVnZlF4UEtKRU0/components_snackbar_specs_fabtablet_002.webm)
Snackbar,可以通过滑动或者点击进行交互,可以看作是比Toast更强大的快速反馈机制,你会发现他们的API非常相似。
```
Snackbar
.make(parentLayout, R.string.snackbar_text, Snackbar.LENGTH_LONG)
.setAction(R.string.snackbar_action, myOnClickListener)
.show(); // Don’t forget to show!
```
你应该注意到了make()方法中把一个View作为第一个参数,Snackbar试图找到一个合适的父亲以确保自己是被放置于底部。
###Tabs
通过选项卡的方式切换[View](http://www.google.com/design/spec/components/tabs.html)并不是Material design中才有的新概念,它们和[顶层导航模式](http://www.google.com/design/spec/patterns/app-structure.html?utm_campaign=io15&utm_source=dac&utm_medium=blog#app-structure-top-level-navigation-strategies)或者组织app中不同分组内容(比如,不同风格的音乐)是同一个概念。

Design library的[TabLayout](http://developer.android.com/reference/android/support/design/widget/TabLayout.html?utm_campaign=io15&utm_source=dac&utm_medium=blog) 既实现了固定的选项卡(View的宽度平均分配),也实现了可滚动的选项卡(View宽度不固定同时可以横向滚动),也可以通过编写代码添加Tab。
```
TabLayout tabLayout = ...;
tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
```
如果,你使用[ViewPager](http://developer.android.com/reference/android/support/v4/view/ViewPager.html?utm_campaign=io15&utm_source=dac&utm_medium=blog)在tab之间横向切换,你可以直接从[PagerAdapter](http://developer.android.com/reference/android/support/v4/view/PagerAdapter.html?utm_campaign=io15&utm_source=dac&utm_medium=blog)的[getPageTitle()](http://developer.android.com/reference/android/support/v4/view/PagerAdapter.html?utm_campaign=io15&utm_source=dac&utm_medium=blog#getPageTitle(int))中创建选项卡,然后使用setupWithViewPager()将两者联系在一起。它可以使tab的选中事件能更新ViewPager,同时ViewPager的页面改变能更新tab的选中状态。
###CoordinatorLayout, 动作和滚动
独特的视觉效果只是Material design小小的一部分:运动也是设计好一款Material designed应用的重要组成部分。而在Material design中,包括[触摸Ripple](http://www.google.com/design/spec/animation/responsive-interaction.html?utm_campaign=io15&utm_source=dac&utm_medium=blog#responsive-interaction-surface-reaction)和[有意义的转场](http://www.google.com/design/spec/animation/meaningful-transitions.html?utm_campaign=io15&utm_source=dac&utm_medium=blog),Design library引入[CoordinatorLayout](http://developer.android.com/reference/android/support/design/widget/CoordinatorLayout.html?utm_campaign=io15&utm_source=dac&utm_medium=blog0),一个从另一层面去控制子view之间触摸事件的布局,Design library中的很多控件都利用了它。
###CoordinatorLayout和Floating Action Buttons
一个很好的例子就是当你将FloatingActionButton作为一个子View添加进CoordinatorLayout并且将CoordinatorLayout传递给 Snackbar.make(),在3.0及其以上的设备上,Snackbar不会显示在悬浮按钮的上面,而是FloatingActionButton利用CoordinatorLayout提供的回调方法,在Snackbar以动画效果进入的时候自动向上移动让出位置,并且在Snackbar动画地消失的时候回到原来的位置,不需要额外的代码。
[example video](http://material-design.storage.googleapis.com/publish/material_v_3/material_ext_publish/0B6Okdz75tqQsLWFucDNlYWEyeW8/components_snackbar_usage_fabdo_002.webm)
CoordinatorLayout还提供了layout_anchor和layout_anchorGravity属性一起配合使用,可以用于放置floating view,比如FloatingActionButton与其他View的相对位置
###CoordinatorLayout 和app bar
另一个比较重要的场合是CoordinatorLayout结合app bar (或者action bar)和 [滚动处理](http://www.google.com/design/spec/patterns/scrolling-techniques.html?utm_campaign=io15&utm_source=dac&utm_medium=blog). 你可能在你的布局里已经使用了[Toolbar](https://developer.android.com/reference/android/support/v7/widget/Toolbar.html?utm_campaign=io15&utm_source=dac&utm_medium=blog), 能让你自定义外观,将应用中最显眼的部分和其他部分整合到一起. Design library采用了进一步的解决方案:使用[AppBarLayout](http://developer.android.com/reference/android/support/design/widget/AppBarLayout.html?utm_campaign=io15&utm_source=dac&utm_medium=blog)可以让Toolbar和其他View(例如展示Tab的TabLayout)对滚动事件作出反应,前提是他们在一个标有ScrollingViewBehavior的View中.因此,你可以创建如下的布局:
```
```
现在,随着用户滚动RecyclerView,AppBarLayout通过子视图上的scroll flag,处理事件作出反应,控制他们如何进入,如何退出。Flag包括:
> - scroll: 所有想滚动出屏幕的view都需要设置这个flag- 没有设置这个flag的view将被固定在屏幕顶部。
- enterAlways: 这个flag让任意向下的滚动都会导致该view变为可见,启用"快速返回"模式。
- enterAlwaysCollapsed: 当你的视图已经设置minHeight属性又使用此标志时,你的视图只会在最小高度处进入,只有当滚动视图到达顶部时才扩大到完整高度。
- exitUntilCollapsed: 在滚动过程中,只有当视图折叠到最小高度的时候,它才退出屏幕。
注意:那些使用Scroll flag的视图必须在其他视图之前声明。这样才能确保所有的视图从顶部撤离,剩下的元素固定在前面(译者注:剩下的元素压在其他元素的上面)。
###折叠 Toolbars
直接向AppBarLayout添加ToolBar,你需要添加enteralwayscollapsed和exituntilcollapsed两个滚动Flag,但是不能在细节上不同的元素对此的反应。为此,您可以使用 [CollapsingToolbarLayout](http://developer.android.com/reference/android/support/design/widget/CollapsingToolbarLayout.html?utm_campaign=io15&utm_source=dac&utm_medium=blog):
```
```
这个设置使用collapsingtoolbarlayout的layout_collapsemode ="pin" 确保在View折叠时,Toolbar本身仍然在屏幕顶部。更好的是,当你同时使用collapsingtoolbarlayout和Toolbar,当布局完全可见时,标题看上去明显变大了;当布局折叠完成后,它恢复到其默认大小。请注意,在这些情况下,你应该调用CollapsingToolbarLayout#settitle() ,而不是调用Toolbar。
[example video](http://material-design.storage.googleapis.com/publish/material_v_3/material_ext_publish/0B0NGgBg38lWWcFhaV1hiSlB4aFU/patterns-scrollingtech-scrolling-070801_Flexible_Space_xhdpi_003.webm)
如果你希望添加压住特定的视图效果,您可以使用app:layout_collapsemode ="parallax"(和app:layout_collapseparallaxmultiplier =“0.7”(可选,用于设置视差乘数)实现视差滚动(也就是说ImageView,作为Toolbar的兄弟节点,在collapsingtoolbarlayout中)。在这种情况下,建议在CollapsingToolbarLayout中设置
app:contentScrim="?attr/colorPrimary"这一属性,这样,当视图折叠的时候,就会有蒙上纱布的渐变效果。
[example video](http://material-design.storage.googleapis.com/publish/material_v_4/material_ext_publish/0B6Okdz75tqQscXNQY3dNdVlYeTQ/patterns-scrolling-techniques_flex_space_image_xhdpi_003.webm)
###CoordinatorLayout与自定义控件
还有一件需要注意的事情,CoordinatorLayout跟FloatingActionButton或AppBarLayout需要一定的配置-它在[Coordinator.Behavior](http://developer.android.com/reference/android/support/design/widget/CoordinatorLayout.Behavior.html?utm_campaign=io15&utm_source=dac&utm_medium=blog)提供了一些API,子视图既可以更好地控制触摸事件也可以通过[onDependentViewChanged()](http://developer.android.com/reference/android/support/design/widget/CoordinatorLayout.Behavior.html?utm_campaign=io15&utm_source=dac&utm_medium=blog#onDependentViewChanged(android.support.design.widget.CoordinatorLayout,%20V,%20android.view.View))给别人提供一个回调方法。
Views可以用CoordinatorLayout.DefaultBehavior(YourView.Behavior.class)注解(annotation)声明默认的Behavior,或者在你的布局文件中声明app:layout_behavior="com.example.app.YourView$Behavior" 属性. 这样做,就可以将任何一个View和CoordinatorLayout整合在一起.
###马上使用吧!
Design library现在就可以使用,请确保已经用SDk Manager更新了Android Support Repository. 然后添加一条dependency,你就可以使用Design library了:
```
compile 'com.android.support:design:22.2.0'
```
需要注意的是,Design library 依赖于Support v4和AppCompat Support Libraries,在你添加 Design library时,这些库也会自动的添加到依赖中。同时,这些控件在Android Studio的Layout Editor (可以在CustomView中找到)中是可用的,你可以便捷的预览一些新的控件。
Design library, AppCompat和所有的Android Support Library 都是开发Android的强有力工具,当你打造一个符合当代风格、好看的应用时,可以使用提供现成的模块,无需从0开始。
================================================
FILE: issue-14/Android之WebRTC介绍.md
================================================
#Android之WebRTC介绍
---
> * 原文链接 : [Introduction to WebRTC on Android](https://tech.appear.in/2015/05/25/Introduction-to-WebRTC-on-Android/)
* 原文作者 : [Dag-Inge Aas](https://disqus.com/home/forums/appearin/)
* 译文出自 : [appear.in](https://tech.appear.in/)
* 译者 : [DorisMinmin](https://github.com/DorisMinmin)
* 校对者: [这里校对者的github用户名](github链接)
* 状态 :完成
WebRTC被誉为是web长期开源开发的一个新启元,是近年来web开发的最重要创新。WebRTC允许Web开发者在其web应用中添加视频聊天或者点对点数据传输,不需要复杂的代码或者昂贵的配置。目前支持Chrome、Firefox和Opera,后续会支持更多的浏览器,它有能力达到数十亿的设备。
然而,WebRTC一直被误解为仅适合于浏览器。事实上,WebRTC最重要的一个特征是允许本地和web应用间的互操作,很少有人使用到这个特性。
本文将探讨如何在自己的Android应用中植入WebRTC,使用[WebRTC Initiative](http://www.webrtc.org/)中提供的本地库。这边文章不会讲解如何使用信号机制建立通话,而是重点探讨Android与浏览器中实现的差异性和相似性。下文将讲解Android中实现对应功能的一些接口。如果想要了解WebRTC的基础知识,强烈推荐[Sam Dutton’s Getting started with WebRTC](http://www.html5rocks.com/en/tutorials/webrtc/basics/)。
##项目中添加WebRTC
*下面的讲解基于Android WebRTC库版本9127.*
首先要做的是在应用中添加WebRTC库。 WebRTC Initiative 提供了[一种简洁的方式来编译](http://www.webrtc.org/native-code/android),但尽量不要采用那种方式。取而代之,建议使用原始的io编译版本,可以从[maven central repository](https://oss.sonatype.org/content/groups/public/io/pristine/)中获取。
添加WebRTC到工程中,需要在你的依赖中添加如下内容:
```Gradle
1 compile 'io.pristine:libjingle:9127@aar'
```
同步工程后,WebRTC库就准备就绪。
##权限
同其他Android应用一样,使用某些 API 需要申请相应权限。WebRTC也不例外。制作的应用不同,或者需要的功能不同,例如音频或者视频,所需要的权限集也是不同的。请确保按需申请!一个好的视频聊天应用权限集如下:
```xml
1
2
3
4
5
6
7
8
9
```
##灯光,摄影,工厂
在浏览器中使用WebRTC时,有一些功能完善、说明详细的API可供使用。 [navigator.getUserMedia](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getUserMedia) 和 [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) 包含了可能用到的几乎所有功能。结合`