为了账号安全,请及时绑定邮箱和手机立即绑定

完美的安卓 model 层架构(下)

标签:
Android

完美的安卓 model 层架构(上)中,我主要介绍了网络请求、数据库持久化、Immutable/Value types、Json 序列化与反序列化这四部分内容,而剩下的关于 Parcelable,ZonedDateTime,null safety,rx error handling,config injection以及测试相关的内容,将在本篇中进行介绍。

声明:本文已独家授权微信公众号Android程序员(AndroidTrending)在微信公众号平台原创首发。

1. 回顾

先回顾一下上篇中的架构:

https://img1.sycdn.imooc.com//5bd40d9d0001ee7b07120501.jpg

总的来说,就是:

·         利用 OkHttp 和 Retrofit 进行网络请求;

·         利用 SqlDelight、AutoValue 及其系列扩展生成我们的 model 类,大大减少我们需要编写的代码;

·         利用 Gson 进行 model 类的 Json 序列化和反序列化;

·         利用 SqlBrite 提供对数据库访问的 reactive API;

·         最后由于 Retrofit 和 SqlBrite 都提供了 reactive API,我们的业务逻辑代码就可以尽情享用响应式编程的强大了。

接下来的部分,就是对上面架构的各个部分的细化和完善。

·         model 类实现 Parcelable 以便于 Activity 和 Fragment 传参;

·         使用 ZonedDateTime 表达时间;

·         Rx Retrofit error processor 统一处理网络错误;

·         model 的 ProGuard 配置注意事项;

·         Config Injection:把业务相关的配置注入到业务无关的 model 架构中;

·         使用 delegate 接口层隔离安卓系统,便于单元测试,以及 UnMock Plugin 和 RestMock 工具的使用;

2. Parcelable

我们的数据类,很可能需要在 Activity,Fragment 以及 View 之间进行传递,一方面作为参数进行传递,另一方面,当发生屏幕旋转,以及其他情况下 Activity 被销毁时,我们需要保存和恢复状态。而这些使用场景中 Parcelable 都扮演着非常重要的角色。

2.1. Activity 传参

Activity 传参的原理就是在构造启动 Activity 的 Intent 时,调用 intent.putExtra() 函数,把参数保存到 Intent 对象中。而在目标 Activity 中,调用 getIntent() 函数获取到 Intent 对象,并从中读取参数。

允许的参数类型除了基本类型(primitive type)及其包装对象类型外,还支持 Serializable 和Parcelable。所以如果我们的数据类型实现了 Parcelable 接口,那就可以直接使用了。

而实现 Parcelable 对于已经集成了 AutoValue 的我们来说,简直不能更简单了。我们只需要引入auto-value-parcel,并让我们的数据类型 implements Parcelable 即可:

[代码]java代码:

?

1

2

3

4

5

@AutoValue

@AutoGson(AutoValue_GithubUser.GsonTypeAdapter.class)

public abstract class GithubUser implements GithubUserModel,   Parcelable {

    // ...

}

auto-value-parcel 和上篇中引入的 auto-value-gson 都是 AutoValue 的一个扩展,作者也是同一个人。经过上述修改之后,auto-value-parcel 会自动为我们生成实现 Parcelable 所需要的代码,在Activity 之间传参的时候,我们直接调用 putExtra 和 getParcelableExtra 即可。

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

final class AutoValue_GithubUser extends $AutoValue_GithubUser   {

    public static final Parcelable.Creator<autovalue_githubuser>   CREATOR =

            new   Parcelable.Creator<autovalue_githubuser>() {

                @Override

                public   AutoValue_GithubUser createFromParcel(Parcel in) {

                    return   new AutoValue_GithubUser(

                            in.readInt()   == 0 ? (Long) in.readSerializable() : null,

                            in.readString(),   in.readString(), in.readString(),

                            in.readInt()   == 0 ? (ZonedDateTime) in.readSerializable() : null);

                }

 

                @Override

                public   AutoValue_GithubUser[] newArray(int size) {

                    return   new AutoValue_GithubUser[size];

                }

            };

 

    @Override

    public void writeToParcel(Parcel   dest, int flags) {

        if (id()   == null) {

            dest.writeInt(1);

        } else {

            dest.writeInt(0);

            dest.writeSerializable(id());

        }

        dest.writeString(login());

        dest.writeString(avatar_url());

        dest.writeString(type());

        if (created_at()   == null) {

            dest.writeInt(1);

        } else {

            dest.writeInt(0);

            dest.writeSerializable(created_at());

        }

    }

 

    @Override

    public int describeContents() {

        return 0;

    }

     

    // ...

}</autovalue_githubuser></autovalue_githubuser>

写过 Activity 传参代码的朋友肯定知道,读写 Intent 中的 extra 都需要指定一个 String 作为 key,代码比较繁琐,因此这里我们可以引入 IntentBuilder,我们只需要为参数添加注解,就可以完成参数传递,让代码瞬间简洁不少,例如这样:

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

@IntentBuilder

class DetailActivity extends Activity {

    @Extra

    String id;

    @Extra @Nullable

    String title;

 

    @Override

    public void onCreate(Bundle   savedInstanceState) {

        super.onCreate(savedInstanceState);

        DetailActivityIntentBuilder.inject(getIntent(),   this);

        //   inject 之后,就可以使用 id 和 title 了

    }

}

 

// ...

 

startActivity(new DetailActivityIntentBuilder("12345")

    .title("MyTitle")

    .build(context))

2.2. Fragment 传参

Fragment 传参和 Activity 原理类似,Fragment 提供了 setArguments() 和 getArguments 方法,而这个 argument,就是 Bundle 对象,只要我们的数据类实现了 Parcelable,就可以通过Bundle 进行传递了。

同样,为了保持代码的简洁,我们可以引入 FragmentArgs,其使用例子如下:

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

@FragmentWithArgs

public class MyFragment extends Fragment {

    @Arg

    int id;

 

    @Override

    public void onCreate(Bundle   savedInstanceState) {

        super.onCreate(savedInstanceState);

        FragmentArgs.inject(this);  

        //   inject 之后,就可以使用 id 了

    }

}

 

// ...

 

MyFragment fragment =   MyFragmentBuilder.newMyFragment(101);

可能有朋友在想,Activity 传参和 Fragment 传参需要使用两个不同的库,能不能把它们统一起来?好消息是这个问题早就有人想到,并且已经造好轮子等着我们来用了:AutoBundle。实际上 AutoBundle 所做的就是把上述两个库整合了起来,因此 AutoBundle 的用法也和它们类似,这里就不赘述了,大家可以自行查看其项目主页。

2.3. ActivityFragment View 的状态保存

Activity,Fragment 和 View 的状态保存使用的同样是 Bundle,而同样也有一个好用的轮子让我们保存状态变得异常简洁:Icepick

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

class ExampleActivity extends Activity {

  @State String username;

 

  @Override public void onCreate(Bundle   savedInstanceState) {

    super.onCreate(savedInstanceState);

    Icepick.restoreInstanceState(this,   savedInstanceState);

  }

 

  @Override public void onSaveInstanceState(Bundle   outState) {

    super.onSaveInstanceState(outState);

    Icepick.saveInstanceState(this,   outState);

  }

}

 

3. ZonedDateTime

细心地朋友可能已经注意到,在上篇中,我们 GithubUser 的 created_at 成员类型是个生面孔:ZonedDateTime。它是何方神圣?

简单来说,Java 8 引入了一套新的 API 来替代 Date 和 Calendar 等 API,代号 JSR-310,JSR-310 的实现者为其提供了一个向后兼容的版本 ThreeTenBP,而由于 ThreeTenBP 把时区数据保存在了 jar 包中,在安卓平台上存在严重的性能问题,因此 JakeWharton 又提供了一个改良版本:ThreeTenABP。ThreeTenABP 就是本节主角了。

为什么要使用 JSR-310 ?因为 Date 和 Calendar 太难用了!计算时间差,判断先后关系,根据已有时间对象创建新的时间对象,都非常麻烦而且极易出错。为什么不用 ThreeTenBP ?上面说了,存在性能问题,更多关于安卓平台读取 jar 包资源性能问题的内容,可以阅读我翻译的 NimbleDroid 团队的博客:揭秘在安卓平台上奇慢无比的 ClassLoader.getResourceAsStream

ZonedDateTime 的具体使用以及 ThreeTenABP 的设置,我就不赘述了,百分百推荐,值得拥有!

4. SqlDelight null safety

细心地朋友可能已经注意到在上篇中,GithubUserModel 和 ZonedDateTimeDelightAdapter 的代码有些奇怪之处:

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

public interface GithubUserModel {

  // ...

   

  @Nullable

  ZonedDateTime created_at();

 

  // ...

}

 

public class ZonedDateTimeDelightAdapter implements ColumnAdapter<zoneddatetime>   {

    // ...

 

    @NonNull

    @Override

    public ZonedDateTime map(final Cursor   cursor, final int columnIndex) {

        return mDateTimeFormatter.parse(cursor.getString(columnIndex),   ZonedDateTime.FROM);

    }

 

    @Override

    public void marshal(final ContentValues   values, final String key,

            @NonNull   final ZonedDateTime value) {

        values.put(key,   mDateTimeFormatter.format(value));

    }

}</zoneddatetime>

在 GithubUserModel 中,created_at 是 Nullable,但是在 ZonedDateTimeDelightAdapter 中,我们却使用了 NonNull,虽然这两个注解并没有强制作用,但我们当然需要遵守它们的语义。

目前 SqlDelight 对 null safety 的处理并不是十分完善,详情可以参考其 Github 项目主页的这个 issue

生成的 Mapper 类已经很好地处理了 null safety 的问题了,但我们需要重写我们 Marshal 类的created_at 方法,来实现处理 created_at 为空的逻辑:

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

final class Mapper<t extends="" githubusermodel="">   implements RowMapper<t> {

    // ...

 

    @Override

    @NonNull

    public T map(@NonNull Cursor   cursor) {

        return creator.create(cursor.isNull(cursor.getColumnIndex(ID))   ? null

                        :   cursor.getLong(cursor.getColumnIndex(ID)),

                cursor.getString(cursor.getColumnIndex(LOGIN)),

                cursor.getString(cursor.getColumnIndex(AVATAR_URL)),

                cursor.getString(cursor.getColumnIndex(TYPE)),

                //   这里已经处理好了 null 的问题

                cursor.isNull(cursor.getColumnIndex(CREATED_AT))   ? null

                        :   created_atAdapter.map(cursor, cursor.getColumnIndex(CREATED_AT)));

    }

 

    // ...

}

 

public static class Marshal extends GithubUserMarshal<marshal>   {

    // ...

 

    @Override

    public Marshal   created_at(@Nullable final ZonedDateTime createdAt) {

        // 如果是 null,就什么也不做

        if (createdAt   == null) {

            return   this;

        }

        return super.created_at(createdAt);

    }

}</marshal></t></t>

在上篇中我们提到,Mapper 负责把 Cursor 对象转换为 GithubUser 对象,Marshal 负责把一个GithubUser 对象转化为一个 ContentValues 对象,用于保存到数据库中。Mapper 和 Marshal 处理好了 null safety 之后,数据库、转换过程、使用方,就都可以很好地处理 null 的问题了,让我们的代码从此少一些 NullPointerException。

5. Rx Retrofit error processor

这一节需要一点 RxJava 的基础,不熟悉的朋友可以先去官网了解一下 RxJava

简而言之,因为网络请求出现错误的可能性很多,其中一部分是服务器返回的 API Error,不同的 API 调用可能需要根据 API Error 的具体内容进行不同处理,而其他的错误则可以统一的处理,例如提示网络异常。因此我们需要一个统一的 handler 来检查是否为 API Error,如果是则需要进行类型转换,以便于使用方进行处理。其次,RxJava 的 Observable subscribe 时需要处理 onError 事件,对于可以统一处理的错误,我们也可以复用一个 RxErrorProcessor。

在 Retrofit 1.x 中,RestAdapter.Builder 有一个 setErrorHandler 方法,可以在 Retrofit 返回的Observable 调用 onError 之前,对错误进行一个集中的判断和转化。但是 Retrofit 2.x 移除了这个接口,我们在 Observable 调用 onError 之前没有机会对错误进行一次集中的转换了。

不过这并不是什么大问题,正好我们还可以把 handler 和 processor 统一起来。所以我们把错误判断与转换和统一处理逻辑都统一到 Subscriber 的 onError 方法中。我们的集中处理逻辑可以是这样的:

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

@Singleton

public class RxNetErrorProcessor implements Action1<throwable>   {

 

    private final Gson mGson;

 

    @Inject

    RxNetErrorProcessor(final Gson   gson) {

        mGson =   gson;

    }

 

    @Override

    public void call(final Throwable   throwable) {

        Timber.e(throwable,   "RxNetErrorProcessor");

    }

 

    public boolean tryWithApiError(final   Throwable throwable, final Action1<apierror> handler) {

        if (throwable   instanceof HttpException) {

            final   HttpException exception = (HttpException) throwable;

            try   {

                final   String errorBody = exception.response().errorBody().string();

                final   ApiError apiError = mGson.fromJson(errorBody, ApiError.class);

                if   (!TextUtils.isEmpty(apiError.message())) {

                    handler.call(apiError);

                    return   true;

                }

            }   catch (Exception e) {

                call(throwable);

            }

        } else {

            call(throwable);

        }

        return false;

    }

}</apierror></throwable>

 

这里我们先判断异常是否为 HttpException,如果是则尝试把 error body 转化为 ApiError 对象,它是服务器定义的错误信息对象,如果是一个合法的 ApiError 对象,我们就使用传入的 handler进行处理,否则我们就使用一个统一的处理方式处理。

结合 lambda 表达式,我们的使用方代码将异常简洁:

[代码]java代码:

?

1

2

3

4

5

6

mGithubUserDao.searchUser(query)

        .subscribeOn(Schedulers.io())

        .observeOn(AndroidSchedulers.mainThread())

        .subscribe(users   -> getView().showSearchResult(users),

                t   -> mRxNetErrorProcessor.tryWithApiError(t,

                        e   -> getView().showError(e.message())));

6. ProGuard

虽然项目是开源的,但商业项目 ProGuard 还是不可或缺的,所以还是需要保证在开启 ProGuard 之后能够正常运行。

在这套 model 层架构中,关于 ProGuard 有几点值得一提:

6.1. AutoGson

由于 auto-value-gson 生成的 GsonTypeAdapter 类的构造函数我们是通过反射进行调用的,所以需要配置保留,否则会被移除,产生问题。而由于 GsonTypeAdapter 类中我们完全没有利用反射进行序列化和反序列化,所以我们可以任由 ProGuard 对成员名进行混淆。

[代码]java代码:

?

1

2

3

4

# AutoGson

-keepclassmembers class **$AutoValue_*$GsonTypeAdapter   {

    void <init>(com.google.gson.Gson);

}</init>

6.2. AutoParcel

安卓系统对 Parcelable 的序列化和反序列化,需要我们的类中有一个名为 CREATOR 的成员,并且它不能进行混淆,否则会报错。

[代码]java代码:

?

1

2

3

4

5

# AutoParcel

-keep class **AutoValue_*$1 { }

-keepclassmembers class * implements android.os.Parcelable   {

    static ** CREATOR;

}

7. Config Injection

在 AndroidTDDBootStrap 项目中,Retrofit,EventBus,SqlBriteDatabase 等对象的创建都在 base module 中,它们的创建需要配置 base url,是否 debug 等参数,但是这些参数都是和业务具体相关的,当然不应该出现在 base module 中,怎么办呢?依赖注入呀!由于注入的是配置参数,所以我称之为 Config Injection。

依赖注入目前应该算是广为人知了,依赖注入可以让我们的代码更加解耦,更 SOLID,更利于测试,依赖注入框架我使用的是 dagger2。关于 dagger2 的使用细节,不熟悉的朋友可以先看一下 dagger2 主页。

下面我以 Retrofit 的配置注入为例,其他的配置注入都与之类似,可以参见项目源码

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

// RetrofitConfig.java,位于 base module 中:

@AutoValue

public abstract class RetrofitConfig {

    public static Builder builder()   {

        return new   AutoValue_RetrofitConfig.Builder();

    }

 

    public abstract String   baseUrl();

 

    @AutoValue.Builder

    public abstract static class Builder   {

        public abstract   Builder baseUrl(final String baseUrl);

 

        public abstract   RetrofitConfig build();

    }

}

 

// ProviderModule.java,位于 base module 中:

@Module

public class ProviderModule {

    // ...

 

    @Singleton

    @Provides

    Retrofit provideRetrofit(final RetrofitConfig   config, final OkHttpClient okHttpClient,

            final   Gson gson) {

        return new   Retrofit.Builder().baseUrl(config.baseUrl())

                .client(okHttpClient)

                .addConverterFactory(GsonConverterFactory.create(gson))

                .addCallAdapterFactory(

                        RxJavaCallAdapterFactory.createWithScheduler(Schedulers.io()))

                .build();

    }

 

    // ...

}

 

// ProviderConfigModule.java,位于 app module 中:

@Module

public class ProviderConfigModule {

 

    private static final boolean DEBUG   = "debug".equals(BuildConfig.BUILD_TYPE);

 

    // ...

 

    @Singleton

    @Provides

    RetrofitConfig   provideRestConfig() {

        return RetrofitConfig.builder().baseUrl(BuildConfig.API_BASE_URL).build();

    }

 

    // ...

}

 

然后我们把 ProviderModule 和 ProviderConfigModule 都添加到目标 component 的 modules 列表中,就可以利用 dagger2 来进行依赖创建和依赖注入了。 是不是非常优雅?

8. 单元测试相关

终于到了最后一部分了:单元测试。为什么要进行单元测试这个问题讲得太多了,我这里就不再多说了。测试相关的内容,主要启发自 Square 团队分享的单元测试系列文章

8.1. The Square Way

square way 的思路很简单,通过引入一层 delegate 接口,我们可以把我们的业务逻辑代码和安卓系统隔离开来,这样我们的业务逻辑代码就和安卓系统没有耦合了,delegate 接口我们可以随意 mock,因此我们就完全可以编写在 JVM 上运行的测试用例。当然也存在 Robolectric 这样的框架,可以在 JVM 上运行安卓测例,但我更倾向于 square way 这样的方式。因为它不仅能让我们更快的执行测试用例,还会让我们的代码更加解耦,更 SOLID。

这里我以 AndroidTDDBootStrap 项目的 com.github.piasy.gh.model.users.dao 包为例,分享 square way 的实践方式。

首先看看包结构:

[代码]java代码:

?

1

2

3

4

5

com.github.piasy.gh.model.users.dao -

        -   DbUserDelegate.java

        -   DbUserDelegateImpl.java

        -   GithubUserDao.java

        -   GithubUserDaoImpl.java

DbUserDelegate 就是负责代理数据库操作的,它的接口如下:

[代码]java代码:

?

1

2

3

4

5

6

7

8

public interface DbUserDelegate {

 

    void deleteAllGithubUser();

 

    void putAllGithubUser(List<githubuser>   users);

 

    Observable<list<githubuser>>   getAllGithubUser();

}</list<githubuser></githubuser>

GithubUserDao 接口的实现如下:

[代码]java代码:

?

1

2

3

4

5

6

7

8

@NonNull

@Override

public Observable<list<githubuser>>   searchUser(@NonNull final String query) {

    return mGithubApi.searchGithubUsers(query,   GithubApi.GITHUB_API_PARAMS_SEARCH_SORT_JOINED,

            GithubApi.GITHUB_API_PARAMS_SEARCH_ORDER_DESC)

            .map(GithubUserSearchResult::items)

            .doOnNext(mDbUserDelegate::putAllGithubUser);

}</list<githubuser>

那么我们需要测试一下正常情况下,搜索到结果之后,会不会被保存到数据库中,如果发生了错误,会不会调用 mDbUserDelegate.putAllGithubUser 接口。由于 delegate 层的存在,我们完全可以编写普通的 JUnit 测试了,测例之一如下:

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

@Test

public void testSearchUserSuccess() {

    // given

    willReturn(Observable.create(new   Observable.OnSubscribe<githubusersearchresult>() {

        @Override

        public void   call(final Subscriber<!--? super GithubUserSearchResult--> subscriber)   {

            subscriber.onNext(mEmptyResult);

            subscriber.onCompleted();

        }

    })).given(mGithubApi).searchGithubUsers(anyString(),   anyString(), anyString());

 

    // when

    final TestSubscriber<list<githubuser>>   subscriber = new TestSubscriber<>();

    mGithubUserDao.searchUser("Piasy").subscribe(subscriber);

    subscriber.awaitTerminalEvent();

 

    // then

    then(mDbUserDelegate).should(timeout(100))

            .putAllGithubUser(anyListOf(GithubUser.class));

    verifyNoMoreInteractions(mDbUserDelegate);

    subscriber.assertNoErrors();

 

    then(mGithubApi).should(timeout(100).only())

            .searchGithubUsers(anyString(),   anyString(), anyString());

}</list<githubuser></githubusersearchresult>

RxJava 也为我们提供了 TestSubscriber 供测试使用,让我们的测例非常简洁。

关于测试,还有两点值得一提。

8.2. UnMock Plugin

运行 JVM/Robolectric 测试的时候,我们经常会遇见 *** not mocked 的错误,这是由于我们在 JVM 上运行测例,如果调用到安卓系统的方法,而且又没有对这些方法进行 mock,就会报这样的错误。而 UnMock Plugin 就是一个致力于解决这个麻烦的 gradle 插件,它可以指定安卓系统代码的实现版本 jar 包,能指定保留哪些类或者方法,这样这些类和方法就可以在测试中被正常调用了。

它的配置方式类似于这样(完整例子请参考其项目主页):

[代码]java代码:

?

1

2

3

4

5

6

7

unMock {

    downloadFrom 'https://oss.sonatype.org/content/groups/public/org/robolectric/android-all/6.0.0_r1-robolectric-0/android-all-6.0.0_r1-robolectric-0.jar'

 

    keep   "android.os.Looper"

    keep   "android.content.ContentValues"

    keepStartingWith   "android.util."

}

首先指定了安卓代码的实现版本,我们使用了 Robolectric 提供的 jar 包,然后我们指定保留 Looper和 ContentValues 类,以及 android.util 包及其子包中的类。

有了 UnMock Plugin,我们在 JVM 上编写安卓单元测试简直如虎添翼!

8.3. RestMock

在 Mockito 官网上有这样一句话:

Don’t mock everything

在编写测试的时候,我们应该尽可能少地进行 mock,尤其是在编写集成测试的时候。例如,能在 OkHttp 层提供 mock 的数据,就不要 mock OkHttpClient,也不要 mock Retrofit,更不要 mock 定义的 RESTful API。

OkHttp 为我们提供了 MockWebServer,让我们可以在 OkHttp 层提供 mock 数据。我曾在之前的文章 (可能是)目前最全面的Android Espresso配置指南了 中介绍过如何使用 MockWebServer 来返回 mock 的数据,但今天我们有了更加便捷的工具:RestMock

RestMock 是对 MockWebServer 的一层封装,让我们可以更加便捷地定义 网络请求要返回的 mock 数据。

一个简单地使用例子是这样(完整例子请参考其项目主页):

[代码]java代码:

?

1

2

RESTMockServer.whenGET(pathStartsWith("/search/users?"))

        .thenReturnString(200,   MockProvider.provideSimplifiedGithubUserSearchResultStr());

是不是超级简洁?而 RestMock 还提供了网络请求路径匹配的强大 matcher,以及网络请求调用的 verifier,让我们 mock 网络请求和网络请求测试从此变得优雅而简洁。

9. 总结

引用一句移动开发每周阅读清单第十一期对上篇文章的介绍:

无论是MVCMVP还是MVVMModel的角色都非常重要,合理的Model设计对整个项目的架构有着至关重要的作用。

这套近乎完美 model 层的架构,终于介绍完了,与其说是提出,说是 发现 更合理。

·         利用 OkHttp 和 Retrofit 进行网络请求;

·         利用 SqlDelight、AutoValue 及其系列扩展生成我们的 model 类;

·         利用 Gson 进行 model 类的 Json 序列化和反序列化;

·         利用 SqlBrite 提供对数据库访问的 reactive API;

·         model 类实现 Parcelable 以便于 Activity 和 Fragment 传参;

·         使用 ZonedDateTime 表达时间;

·         Rx Retrofit error processor 统一处理网络错误;

·         model 的 ProGuard 配置注意事项;

·         Config Injection:把业务相关的配置注入到业务无关的 model 架构中;

·         使用 delegate 接口层隔离安卓系统,便于单元测试,以及 UnMock Plugin 和 RestMock 工具的使用;

转自:Piasy

 

原文链接:http://www.apkbus.com/blog-705730-60550.html

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消