Android编程权威指南总结(七)

第二十章      数据绑定与MVVM

一、为何要用MVVM架构

      目前为止,我们开发的应用都使用了简单版的MVC架构,MVC架构比较适合小规模、简单型的应用,它方便开发人员理清项目结构,快速添加新功能,为开发打下坚实基础。应用因此得以快速完成并投入使用,在项目早期阶段能保持稳定运行。

      但是,随着应用越变越复杂,fragment和activity开始膨胀,逐渐变得难以理解和扩展,添加新功能或改bug变得困难,这个时候,控制器层就需要做功能拆分了。

      怎么拆?先搞清楚控制器类到底做了哪些工作,再把这些工作拆分到独立的小类里,让一个个拆开的小类协同工作。

      MVVM架构很好的把控制器里的臃肿代码抽到布局文件里,让开发人员很容易看出哪些是动态界面,同时,它也抽出部分动态控制器代码放入 ViewModel 类 ,这大大方便了开发测试和验证。

      每个视图模型应控制成多大规模,这要具体情况具体分析。如果视图模型过大,你还可以继续拆分,总之,自己的架构需要自己把控,即使大家都使用MVVM架构,业务不同,场景不一样,每个人的具体实现方法都有差异。

      MVVM架构,Model层:数据模型(实体类、持久化、IO);View层:Activity/Fragment 和布局文件;ViewModel层:业务逻辑的处理、数据的转换、连接M层和V层的桥梁。

通过以上两图我们可以看出:

  1. View持有ViewModel引用;
  2. ViewModel持有Model引用;
  3. View与ViewModel存在一对多关系;
  4. ViewModel与Model存在一对多关系。

二、创建BeatBox应用

      以下的内容都根据BeatBox应用来讨论,至于应用如何创建以及一些平常的代码,这里就不写了,大家都知道~~

1、简单的数据绑定

      将xml布局文件与RecyclerView关联起来,我们用数据绑定来做,比以前用的方式要方便快捷。

      首先,在应用的 build.gradle 文件里启用数据绑定:

android{

    ......

    dataBinding { 
        enabled = true 
    }
}

      这会打开IDE的整合功能,允许你使用数据绑定产生的类,并把它们整合到编译里去。

      要在布局里使用数据绑定,首先要把一般布局改造为数据绑定布局。具体做法就是把整个布局定义放入 <layout> 标签:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </android.support.v7.widget.RecyclerView>
</layout>

      <layout> 告诉数据绑定工具:这个布局由你来处理。接到任务,数据绑定工具会帮你生成一个绑定类,新产生的绑定类默认以布局文件命名。当然,不是snake_case这种格式,而是CamelCase格式(类名嘛)。

      完了之后,fragment_beat_box.xml 已经有了一个叫 FragmentBeatBoxBinding 的绑定类,这就是要用来做数据绑定的类。现在,实例化视图层级结构时,不再使用 LayoutInflater,而是实例化 FragmentBeatBoxBinding 类。在一个叫做 getRoot() 的getter方法里,FragmentBeatBoxBinding 引用着布局视图结构,而且也会引用那些在布局文件里以  android:id  标签引用的其他视图。

      所以,目前 FragmentBeatBoxBinding 类有两个引用:getRoot() 和 recyclerView ,前者指整个布局,后者指RecyclerView,如下图所示:

      由于当前布局只有一个视图,所以两个引用都指向了同一个视图:RecyclerView。

      下面,开始使用这个绑定类并配置 RecyclerView:

@Override 
public View onCreateView(LayoutInflater inflater, ViewGroup container, 
                                                               Bundle savedInstanceState) { 
    FragmentBeatBoxBinding binding = DataBindingUtil.inflate(inflater,             
                                            R.layout.fragment_beat_box, container, false); 
    binding.recyclerView.setLayoutManager(new GridLayoutManager(getActivity(),3)); 
    return binding.getRoot(); 
}

      简单的数据绑定已完成。

三、导入assets

      assets里面可以存放资源文件。可以把assets想象为经过精简的资源:它们也像资源那样打入APK包,但不需要配置系统工具管理。使用assets有两面性:一方面,无需配置管理,可以随意命名assets,并按自己的文件结构组织它们;另一方面,没有配置管理,无法自动响应屏幕显示密度、语言这样的设备配置变更,自然也就无法在布局或其他资源里自动使用它们了。

      所以,总体上讲,资源系统是更好的选择。然而,如果只想在代码中直接调用文件,那么assets就有优势了。大多数游戏就是使用assets加载大量图片和声音资源。

四、处理assets

      assets 导入后,还要能在应用中进行定位、管理记录以及播放。这需要新建一个名为 BeatBox 的资源管理类。使用 AssetManager 类访问 assets:mAssets = context.getAssets()

      通常,在访问assets时,可以不用关心究竟使用哪个 Context 对象,因为,在实践中的任何场景下,所有 Context 中的 AssetManager 都管理着同一套assets资源。

      要取得assets中的资源清单,可以使用 list(String) 方法:

String[] soundNames = mAssets.list("文件夹名称");

      此方法能列出指定目录下的所有文件名。因此,只要传入资源所在的目录,就能看到其中的文件。

五、使用assets

六、绑定数据

      使用数据绑定,我们还可以在布局文件中声明数据对象:

<layout xmlns:android="http://schemas.android.com/apk/res/android" 
        xmlns:tools="http://schemas.android.com/tools"> 
    <data> 
        <variable 
            name="crime" 
            type="com.bignerdranch.android.criminalintent.Crime"/> 
    </data> 
    ... 
</layout>

      然后,使用绑定操作符@{}就可以在布局文件中直接使用这些数据对象的值:

<CheckBox 
    android:id="@+id/list_item_crime_solved_check_box"
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:layout_alignParentRight="true" 
    android:checked="@{crime.isSolved()}" 
    android:padding="4dp"/>

      在对象关系图中,可以这样表示:

      使用数据绑定,最直接的就是绑定布局文件中的Bean对象:

      然而,这似乎有架构问题,首先,从MVC视角看看问题在哪儿:

      不管是哪种架构,有一个指导原则都一样:责任单一性原则。也就是说,每个类应该只负责一件事情。按此原则,MVC是这样落实的:模型表明应用是如何工作的;控制器决定如何显示应用;视图显示你想看到的结果。 

      使用上面图示的数据绑定,就破坏了责任划分。这是因为Sound模型对象不可避免地需要关心显示问题。代码也就此开始混乱了。模型层代码和控制器层代码里都是Sound.java。

      为了避免破坏单一性原则,我们引入一种叫做视图模型的新对象(配合数据绑定使用)。视图模型负责如何显示视图:

      这种架构称为MVVM。控制器对象格式化视图数据的工作转给了视图模型对象。现在,使用数据绑定,组件关联数据就能直接在布局文件里处理了。控制器对象(fragment或activity)开始负责初始化布局绑定类和视图模型对象,同时也是它们之间的纽带。

1、创建视图模型

      首先,来创建视图模型类。创建一个名为SoundViewModel的新类,然后添加两个属性:一个Sound对象,一个播放声音文件的BeatBox对象。对于该项目来说,还需要一个额外的方法获取Sound的文件名:

public class SoundViewModel { 
    private Sound mSound; 
    private BeatBox mBeatBox; 
 
    public SoundViewModel(BeatBox beatBox) { 
        mBeatBox = beatBox; 
    } 

    //获取Sound的文件名
    public String getTitle() { 
        return mSound.getName(); 
    }
 
    public Sound getSound() { 
        return mSound; 
    } 
 
    public void setSound(Sound sound) { 
        mSound = sound; 
    } 
}

2、绑定至视图模型

      现在,把视图模型整合到布局文件里。第一步是在布局文件里声明属性,这在绑定类上定义了一个叫 viewModel 的属性,同时还包括getter和setter方法。在绑定类里,可以用绑定表达式使用 viewModel。

<layout xmlns:android="http://schemas.android.com/apk/res/android" 
        xmlns:tools="http://schemas.android.com/tools"> 
    <data> 
        <variable 
            name="viewModel" 
            type="com.bignerdranch.android.beatbox.SoundViewModel"/> 
    </data> 
 
    <Button 
        android:layout_width="match_parent" 
        android:layout_height="120dp" 
        android:text="@{viewModel.title}" 
        tools:text="Sound name"/> 
</layout>

      在绑定表达式里,可以写一些简单的Java表达式,如链式方法调用、数学计算等。另外,也可以吃几颗“语法糖”。例如,上述viewModel.title实际就是viewModel.getTitle()的简写形式。数据绑定知道怎么帮你翻译。

      最后一步就是关联使用视图模型。

      在SoundHolder构造方法里,我们创建并添加了一个视图模型。然后,在绑定方法里,更新视图模型要用到的数据。一般不需要调用executePendingBindings()方法。然而在这里,我们正在RecyclerView里更新绑定数据。这样,RecyclerView的表现就更为流畅。

      最后,实现onBindViewHolder(...)方法以使用视图模型:

      这些都是根据自己项目的逻辑来设计,遵循原则,让代码干净整洁。

3、绑定数据观察

      此时,还有问题没有解决。上下反复滚动几次应用页面,会看到重复的文件名。因为我们在SoundHolder.bind(Sound)方法里更新了SoundViewModelSound,但布局不知道,从上面图20-10可知,视图模型并不会给布局反馈信息,而我们用的数据绑定的方式,是在布局文件里设置的Button要显示的文件名,所以可能会出现重复。

      现在任务明确了,我们要让它们沟通起来。有一个比较好的做法:使用数据绑定的 BaseObservable 类。

      使用 BaseObservable 类需要三个步骤:

  • 在视图模型里继承 BaseObservable 类
  • 使用 @Bindable 注解视图模型里可绑定的属性
  • 每次可绑定的属性值改变时,就调用 notifyChange() 方法或 notifyPropertyChanged( int ) 方法

      这里,调用 notifyChange() 方法,就是通知绑定类,视图模型对象上所有可绑定属性都已更新。据此,绑定类会再次运行绑定表达式更新视图数据。

      至于 notifyPropertyChanged( int ) 方法,它和 notifyChange() 方法做同样的事,但覆盖面不一样。调用 notifyChange() 方法,相当于是说:“所有的可绑定属性都变了,请全部更新”。调用 notifyPropertyChanged( int ) 方法是说:“只有已绑定的属性值有变化”。

七、访问assets

      assets的工作原理:

      尝试使用File对象打开资源文件是行不通的。正确的方式是使用AssetManager,这样才能得到标准的InputStream数据流:

String assetPath = sound.getAssetPath(); 
InputStream soundData = mAssets.open(assetPath);
       不过,有些 API 可能还需要 FileDescriptor。改用 AssetManager.openFd(String) 方法就行了:
String assetPath = sound.getAssetPath(); 
// AssetFileDescriptors are different from FileDescriptors, 
AssetFileDescriptor assetFd = mAssets.openFd(assetPath); 
// but you get can a regular FileDescriptor easily if you need to. 
FileDescriptor fd = assetFd.getFileDescriptor();

八、深入学习:数据绑定再探

      数据绑定还有更多的表达式和语法,有兴趣可以多了解一下。

九、深入学习:为何使用assets

      事实上,应用也可以使用Android资源处理。资源可以存储声音文件,比如在 res/raw 目录下保存79_long_scream.wav文件后,就可以使用像R.raw.79_long_scream这样的ID取到它。声音文件存储为资源后,就可以像使用其他资源那样使用它们了。

      那为何使用assets呢?这是因为,当项目需要大量资源文件时,如果使用Android资源系统一个个去处理,效率会非常低,要是这些文件能全放在一个目录下管理就好了,可惜资源系统不允许这么做。而assets可以看作随应用打包的微型文件系统,支持任意意层次的文件目录结构。因为这个优点,assets常用来加载大量图片和声音资源,比如游戏。

 

第二十一章      音频播放与单元测试

      MVVM架构极大方便了一项关键编程工作:单元测试。单元测试是指编写小程序去验证应用各个单元的独立行为。

      SoundPool定制版实用工具,能加载一批声音资源到内存中,并能控制同时播放的音频文件的个数。

一、创建SoundPool

mSoundPool = new SoundPool(MAX_SOUNDS,AudioManager.STREAM_MUSIC, 0);

      Lollipop引入了新的SoundPool创建方式:使用SoundPool.Builder。不过,为了兼容API 19这一最低级别,还是要用SoundPool(int, int, int)这个老构造方法。

      第一个参数指定同时播放多少个音频。这里指定了5个。已经播放了5个音频时,如果尝试再播第6个,SoundPool会停止播放原来的音频。

      第二个参数确定音频流类型。Android有很多不同的音频流,它们都有各自独立的音量控制选项。这就是调低音乐音量之后闹钟音量却不受影响的原因。STREAM_MUSIC是音乐和游戏常用的音量控制常量。

      最后一个参数指定采样率转换品质。参考文档说这个参数不起作用,所以这里传入0

二、加载音频文件

      SoundPool有个快速响应的优势:指令刚一发出,它就立即开始播放,一点都不拖沓。不过反应快也要付出代价,那就是在播放前必须预先加载音频。SoundPool加载的音频文件都有自己的IntegerID。在Sound类中添加mSoundId实例变量,并添加相应的getter方法和setter方法管理这些ID

      注意,mSoundIdInteger类型而不是int。这样,在SoundmSoundId没有值时,可以设置其为null值。

      调用mSoundPool.load(AssetFileDescriptor, int)方法可以把文件载入SoundPool待播。为了方便管理、重播或卸载音频文件,mSoundPool.load(...)方法会返回一个intID。这实际就是存储在mSoundId中的ID

private void load(Sound sound) throws IOException {
    AssetFileDescriptor afd = mAssets.openFd(sound.getAssetPath());
    int soundId = mSoundPool.load(afd, 1);
    sound.setSoundId(soundId);
}

三、播放音频

public void play(Sound sound) { 
    Integer soundId = sound.getSoundId(); 
    if (soundId == null) { 
        return; 
    } 
    mSoundPool.play(soundId, 1.0f, 1.0f, 1, 0, 1.0f); 
}

      播放前,要检查并确保soundId不是null值。Sound加载失败会出现null值的情况。

      检查通过后,就可以调用SoundPool.play(int, float, float, int, int, float)方法播放音频了。这些参数依次是:音频ID、左音量、右音量、优先级(无效)、是否循环以及播放速率。我们需要最大音量和常速播放,所以传入值1.0。是否循环参数传入0,代表不循环(如果想无限循环,可以传入-1)。

四、添加测试依赖

      要编写测试代码,首先需要添加两个测试工具:MockitoHamcrestMockito是一个方便创建虚拟对象的Java框架。有了虚拟对象,就可以单独测试SoundViewModel,不用担心会因代码关联关系测到其他对象。

      Hamcrest是个规则匹配器工具库。匹配器可以方便地在代码里模拟匹配条件。如果不能按预期匹配条件定义,测试就通不过。这可以验证代码是否按预期工作。

      有这两个依赖库就可以做单元测试了,现在就来添加。右键单击app模块,选择Open Module Settings菜单项。选择弹出界面里的Dependencies选项页,然后点击+按钮弹出选择依赖库窗口,输入mockito后搜索,选 择org.mockito:mockito-core依赖库,点击OK按钮完成添加。重复上述步骤,搜索hamcrest-junit并选择org.hamcrest:hamcrest-junit完成对Hamcrest依赖库的添加。

      但是有的可能搜索不到,可以直接在 app/build.gradle 里面添加依赖:

testCompile 'org.mockito:mockito-core:2.2.1' 
testCompile 'org.hamcrest:hamcrest-junit:2.0.0.0'

      testCompile作用范围表示,这两个依赖项只需包括在应用的测试编译里。这样就能避免在APK包里捎带上无用代码库。

五、创建测试类

      写单元测试最方便的方式是使用测试框架。使用测试框架可以集中编写和运行测试案例,并支持在Android Studio里看到测试结果。JUnit是最常用的Android单元测试框架,能和Android Studio无缝整合。要用它测试,首先要创建一个用作JUnit测试的测试类。打开 SoundViewModel.java 文件,使用Command+Shift+T(Ctrl+Shift+T)组合键。Android Studio会尝试寻找这个类关联的测试类。如果找不到,它就会提示新建。选择Create New Test...创建一个新测试类。测试库选择JUnit4,勾选setUp/@Before,其他保持默认设置。(若是快捷键不能用,可以在 SoundViewModel.java 类中点击右键--->Go To--->Test)

      点击OK按钮,进入下一个对话框。

      最后一步是选择创建哪种测试类,或者说选择哪个测试目录存放测试类(androidTesttest)。在androidTest目录下的都是整合测试类。整合测试可以运行在设备或虚拟设备上。这样做有优点:可以在运行时动态测试应用行为。但也有缺点:需要编译打包为APK在设备上运行,浪费资源。

      在test目录下的是单元测试类。单元测试运行在本地开发机上,可以脱离Android运行时环境,因此速度会快很多。单元测试的规模最小:测试单个类。所以,单元测试不需要运行整个应用或支持设备,可以不影响手头工作,快速反复地执行。考虑到这个因素,我们选择test目录存放测试类,最后点击OK按钮完成。

六、实现测试类

      测试框架创建的模板类只有一个setUp()方法,和大多数对象一样,测试类也需要创建对象实例以及它依赖的其他对象。为了避免为每一个测试类写重复代码,JUnit提供了@Before这个注解。以@Before注解的包含公共代码的方法会在所有测试之前运行一次。按照约定,所有单元测试类都要有以@Before注解的setUp()方法。

1、使用虚拟依赖项

      setUp()方法里,我们会创建一个SoundViewModel实例用来测试。这需要BeatBox实例,因为SoundViewModel需要BeatBox作为构造参数。如果在单元测试中创建BeatBox实例,会有一个问题:如果BeatBox出了问题,很明显,用到它的测试代码都会出错。

      解决办法很简单:使用虚拟BeatBox。虚拟对象会继承BeatBox,有同样的方法,但这些方法啥事都不干。这样,依赖BeatBoxSoundViewModel测试就不会有问题了。

      要用Mockito创建虚拟对象,需要传入要虚拟的类,调用mock(Class)静态方法。创建一个虚拟BeatBox对象并存入mBeatBox变量。使用mock(Class)方法需要导入支持包。mock(Class)方法会自动创建一个虚拟版本的BeatBox。

      有了虚拟依赖对象,现在来完成SoundViewModel测试类。创建一个SoundViewModel和一个Sound备用(Sound是简单的数据对象,不容易出问题,这里就不虚拟它了):

public class SoundViewModelTest { 
    private BeatBox mBeatBox; 
    private Sound mSound; 
    private SoundViewModel mSubject; 
 
    @Before 
    public void setUp() throws Exception { 
        mBeatBox = mock(BeatBox.class); 
        mSound = new Sound("assetPath"); 
        mSubject = new SoundViewModel(mBeatBox); 
        mSubject.setSound(mSound); 
    } 
}

      注意,我们平常声明SoundViewModel类型变量时,命名一般是mSoundViewModel。这里,我们用了mSubject。这是一种习惯约定,这样做的原因有两点:

  • 很清楚就知道,mSubject是要测试的对象(与其他对象区别开来);
  • 如果SoundViewModel里有任何方法要移到其他类,比如BeatBoxSoundViewModel,那么测试方法可以直接复制过去,省了mSoundViewModelmBeatBoxSoundViewModel重命名的麻烦。

七、编写测试方法

      实际上,就是在测试类里写一个以@Test注解的测试方法。首先写一个方法,断定SoundViewModel里的getTitle()属性和Sound里的getName()属性是有关系的:

@Test 
public void exposesSoundNameAsTitle() { 
    assertThat(mSubject.getTitle(), is(mSound.getName())); 
}

      注意,窗口里有两个方法会变红:assertThat(...)方法和is(...)方法。在assertThat(...)方法上使用 Option+Return Alt+Enter )组合键,然后选 Static import method... ,然后选hamcrest-core-l.3库里的MatcherAssert.assertThat(...)方法。以同样方式,选hamcrest-core-l.3库里的Is.is方法。

      这个测试方法使用了Hamcrest匹配器的is(...)方法和JUnitassertThat(...)方法。方法体里的代码很直白:断定测试对象获取标题方法和sound的获取文件名方法返回相同的值。如果不同,单元测试失败。

      为了运行测试,右键点击app/java/com.bignerdranch.android.beatbox (test),然后选择Run 'Tests in 'beatbox''。随后,一个结果窗口弹出,测试通过。

1、测试对象交互

      实践中,通常的做法是,在写新方法之前,先写一个测试验证这个方法的预期结果。我们需要在SoundViewModel类里写onButtonClicked()方法去调用BeatBox.play(Sound)方法。先SoundViewModel类里新建一个 onButtonClicked() 空方法放着。

      单元测试方法会调用这个方法,而且,也应验证这个方法的实际作用:调用BeatBox.play(Sound)方法。这种繁琐的事就交给Mockito吧!对于每次调用,所有的Mockito虚拟对象都能自我跟踪管理哪些方法调用了,以及都传入了哪些参数。

      调用verify(Object)方法,确认onButtonClicked()方法调用了BeatBox.play(Sound)方法。

@Test 
public void callsBeatBoxPlayOnButtonClicked() { 
    mSubject.onButtonClicked(); 

    //验证以mSound作为参数,调用了mBeatBox对象的play(...)方法
    verify(mBeatBox).play(mSound); 
}

      SoundViewModel.onButtonClicked()是个空方法,所以,什么也没发生。这意味着测试应该会失败。运行测试看结果,verify(Object)做出某个断定,但断定无效,测试失败并给出问题原因日志。

      之后,实现onButtonClicked()方法,让测试符合预期:

public void onButtonClicked() { 
    mBeatBox.play(mSound); 
}

      再次运行,测试通过。

八、数据绑定回调

      按钮要响应事件还差最后一步:关联按钮对象和onButtonClicked()方法。继续使用数据绑定的方法来关联按钮和点击监听器:

<Button 
    android:layout_width="match_parent" 
    android:layout_height="120dp" 

    android:onClick="@{() -> viewModel.onButtonClicked()}" 

    android:text="@{viewModel.title}" 
    tools:text="Sound name"/>

      现在,如果运行应用,按钮就能播放声音。然而,如果你尝试使用绿色的运行按钮,测试又运行了。这是因为右键点击运行测试修改了运行配置。这个配置决定点击绿色按钮之后,Android Studio该运行什么。为了运行BeatBox应用,点击Run按钮旁边的配置选择器,切换至app运行配置。

九、释放音频

      音频播放完毕,应调用SoundPool.release() 方法释放SoundPool

十、设备旋转和对象保存

      正在播放音频的过程中,旋转设备,声音就会停止。因为设备旋转时,Activity和Fragment都会被销毁。之前使用过用onSaveInstanceState(Bundle)方法解决这个问题,但是这里不行。因为这种方法需要首先保存数据,然后再使用Bundle中的Parcelable恢复数据。

      类似于SerializableParcelable是一个把对象以字节流的方式保存的API。对于可保存对象,可以让它实现Parcelable接口。在Java世界,要保存对象,要么将其放入Bundle中,要么实现Serializable接口或者Parcelable接口。无论采用哪种方式,对象首先要是可保存对象。

      怎么理解可保存呢?举个例子你就明白了。你和朋友在看电视节目。什么频道、音量大小,甚至是电视型号,你都可以记下来。如此一来,就算发生火灾或停电这样的事情,等一切恢复正常,看看记下的信息,你依然能接着看原来的电视节目。显然,当前所看的电视节目的配置是可保存的,而观看节目的那段时间却无法保存:一旦发生火灾或停电,其间那段时光就流逝掉了。就算恢复观看,流逝掉的那段时光再也找不回来了。

      BeatBox 项目的某些部分可以保存,例如,Sound类中的一切都可以保存;而SoundPool就无法保存了。虽然可以新建包含同样音频文件的SoundPool,甚至能从音频播放中断处继续,你还是会体验到被打断的滋味。这是改变不了的事实。所以说,SoundPool是无法保存的。

      不可保存性有向外传递的倾向。如果一个对象重度依赖另一个不可保存的对象,那么这个对象很可能也无法保存。BeatBoxSoundPool就是这样的一对。SoundPool要依靠BeatBox播放音频。基于这个事实,可以证明BeatBox也是无法保存的。

      普通的savedInstanceState机制只适用于可保存的对象数据,但BeatBox不可保存。在Activity创建和销毁时,BeatBox实例需要持续可用。

      这个难题该怎么解决呢?

1、保留 fragment

      为了应对设备配置变化,fragment有一个特殊方法可确保BeatBox实例不被销毁,这个方法就是retainInstance

@Override 
public void onCreate(Bundle savedInstanceState) { 
    super.onCreate(savedInstanceState); 

    setRetainInstance(true); 

    mBeatBox = new BeatBox(getActivity()); 
}

      fragmentretainInstance属性值默认为false,这表明其不会被保留。调用setRetainInstance(true)方法可保留fragment已保留fragment不会随activity一起被销毁。相反,它会一直保留,并在需要时原封不动地转给新的activity。对于已保留的fragment实例,其全部实例变量(如mBeatBox)的值也会保持不变,因此可放心继续使用。

2、设备旋转和已保留的 fragment

      我们来看看保留fragment的工作原理。fragment之所以能保留,是因为:可以销毁和重建fragment的视图,但fragment自身可以不被销毁。

      设备配置发生改变时,FragmentManager首先销毁队列中fragment的视图。在设备配置改变时,总是销毁与重建fragmentactivity的视图,这都是基于同样的理由:新的配置可能需要新的资源来匹配;当有更合适的资源可用时,则应重建视图。

      紧接着,FragmentManager检查每个fragmentretainInstance属性值。如果属性值为false(初始默认值),FragmentManager会立即销毁该fragment实例。随后,为了适应新的设备配置,新activity的新FragmentManager会创建一个新的fragment及其视图:

      如果属性值为true,则该fragment的视图立即被销毁,但fragment本身不会被销毁。为了适应新的设备配置,新activity创建后,新FragmentManager会找到已保留的fragment,并重新创建它的视图:

      虽然已保留的fragment没有被销毁,但它已脱离消亡中的activity并处于保留状态。尽管此时的fragment还在,但已没有任何activity托管它:

      必须同时满足以下两个条件,fragment才能进入保留状态:

  • 已调用了fragment的 setRetainInstance(true) 方法;
  • 因设备配置改变(通常为设备旋转),托管activity正在被销毁。

      fragment只能保留非常短的时间,即从fragment脱离旧activity到重新附加给快速新建的activity之间的一段时间。

十一、深入学习:是否保留 fragment

      你可能会疑惑:为什么不保留所有fragment?为什么 fragment 的 retainInstance 默认属性值不是true?这是因为,除非万不得已,最好不要使用这种机制。

      首先,相比非保留 fragment,已保留 fragment 用起来更复杂。一旦出现问题,排查非常耗时。既然它会让程序变得复杂,能不用就不用吧。

      其次,fragment在使用保存实例状态的方式处理设备旋转时,也能够应对所有生命周期场景;但保留的fragment只能应付activity因设备旋转而被销毁的情况。如果activity是因系统回收内存而被销毁,则所有保留的fragment也会随之被销毁,数据也就跟着丢失了。

十二、深入学习:Espresso 与整合测试

      在单元测试里,受测对象都是单个类。在整合测试里,受测对象是整个应用。通常测试要覆盖每个页面。

      EspressoGoogle开发的一个UI测试框架,可用来测试Android应用。在app/build.gradle文件中,添加com.android.support.test.espresso:espresso-core依赖项,作用范围改为androidTestCompile,就可以引入它。引入Espresso之后,就可以用它来测试某个activity的行为。

      至于具体用法,若是需要可以去详细了解。

      单元测试和整合测试用处各异。单元测试简单快速,多用用就会形成习惯,所以能让大多数人接受并喜欢。整合测试需要花很多时间,不适合做经常性的测试。然而,不管怎样,这两类测试都很重要,各自能从不同视角检验应用。所以,只要有条件,二者都不能少。

十三、深入学习:虚拟对象与测试

      相比单元测试,虚拟对象在整合测试中扮演了更为不寻常的角色。虚拟对象假扮成其他不相干的组件,其作用就是隔离受测对象。单元测试的受测对象是单个类;每个类都有自己不同的依赖关系,所以,每个受测类也有一套不同于其他类的虚拟对象。既然都是些不同的虚拟对象,那么它们各自的具体行为怎么样,怎么实现,一点也不重要。所以,对于单元测试来说,一些虚拟化框架,比如能快速创建虚拟对象的Mockito,就非常有用了。

      再来看整合测试。在整合测试场景中,虚拟对象显然不能用来隔离应用,相反,我们用它把应用和可能的外部交互对象隔离开来,如提供web service假数据和假反馈。如果是在BeatBox应用里,你很可能就要提供虚拟SoundPool,让它告诉你某个声音文件何时播放。显然,相比常见的行为虚拟,这种虚拟太重了,而且还要在很多整合测试里共享。这真不如手动写假对象。所以, 做整合测试时,最好避免使用像Mockito这样的自动虚拟测试框架。

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值