Android單元測(cè)試之Robolectric

前言

在博客Android單元測(cè)試之PowerMockito斟珊,主要介紹PowerMockito的使用和對(duì)Java測(cè)試用例的強(qiáng)大支持。但對(duì)于Android app開發(fā)來說,寫起單元測(cè)試很痛苦:一方面單元測(cè)試需要運(yùn)行在模擬器上或者真機(jī)上,不僅麻煩而且緩慢;另一方面菱农,一些依賴Android SDK的對(duì)象(如Activity,Button等)的測(cè)試非常頭疼柿估。Robolectric可以解決此類問題循未,它的設(shè)計(jì)思路便是通過實(shí)現(xiàn)一套JVM能運(yùn)行的Android代碼,從而做到脫離Android環(huán)境進(jìn)行測(cè)試秫舌。本文將結(jié)合項(xiàng)目對(duì)Robolectric做一個(gè)簡(jiǎn)單介紹的妖,并列舉在實(shí)踐踩的各種坑。

Robolectric簡(jiǎn)介

我們可以使用Android提供的Instrumentation系統(tǒng)如ActivityUnitTestCase足陨、ActivityInstrumentationTestCase2嫂粟,將單元測(cè)試代碼運(yùn)行在模擬器或者是真機(jī)上。雖然這種方式可以work墨缘,但是速度非常慢星虹,因?yàn)槊看芜\(yùn)行一次單元測(cè)試,都需要將整個(gè)項(xiàng)目打包成apk镊讼,上傳到模擬器或真機(jī)上宽涌,就跟運(yùn)行了一次app似得,這個(gè)顯然不是單元測(cè)試該有的速度蝶棋。此外卸亮,Google開源的測(cè)試框架如UIAutomatorEspresso也是基于Instrumentation的,更偏向于UI方面的自測(cè)化測(cè)試玩裙,要是應(yīng)用在單元測(cè)試上速度也是不敢恭維的兼贸。

對(duì)了段直,說一句題外話,感興趣的同學(xué)可以看一下ActivityUnitTestCase和ActivityInstrumentationTestCase2的源碼寝受,你會(huì)驚奇地發(fā)現(xiàn)坷牛,它們的實(shí)現(xiàn)方式還是有所區(qū)別罕偎,雖然都是依賴Instrumentation把Activity加載起來很澄,運(yùn)行在同一個(gè)進(jìn)程中,但ActivityUnitTestCase是運(yùn)行在UI主線程中的颜及,而ActivityInstrumentationTestCase2是運(yùn)行在子線程中的甩苛,所以在實(shí)際的使用中還是有區(qū)別的,ActivityUnitTestCase可以直接操控UI俏站,而ActivityInstrumentationTestCase2則是不行讯蒲,需要借助于runOnUiThread()方法來更新UI,否則會(huì)拋異常肄扎。

言歸正傳吧墨林,我們還是接著說Robolectric。Robolectric通過實(shí)現(xiàn)一套JVM能運(yùn)行的Android代碼犯祠,然后在unit test運(yùn)行的時(shí)候去截取android相關(guān)的代碼調(diào)用旭等,然后轉(zhuǎn)到自己實(shí)現(xiàn)的代碼去執(zhí)行這個(gè)調(diào)用的過程。舉個(gè)例子說明一下衡载,比如Android里面有個(gè)類叫Button搔耕,Robolectric則實(shí)現(xiàn)了一個(gè)叫ShadowButton類。這個(gè)類基本上實(shí)現(xiàn)了Button的所有公共接口痰娱。假設(shè)你在unit test里面寫到String text = button.getText().toString();弃榨,在這個(gè)unit test運(yùn)行時(shí),Robolectric會(huì)自動(dòng)判斷你調(diào)用了Android相關(guān)的代碼button.getText()梨睁,在底層截取這個(gè)調(diào)用過程鲸睛,轉(zhuǎn)到ShadowButton的getText方法來執(zhí)行。而ShadowButton是真正實(shí)現(xiàn)了getText這個(gè)方法的坡贺,所以這個(gè)過程便可以正常執(zhí)行官辈。

除了實(shí)現(xiàn)Android里面的類的現(xiàn)有接口,Robolectric還做了另外一件事情拴念,極大地方便了unit testing的工作钧萍。那就是他們給每個(gè)Shadow類額外增加了很多接口,方便我們讀取對(duì)應(yīng)Android類的一些狀態(tài)政鼠。比如ImageView有一個(gè)方法叫setImageResource(resourceId)风瘦,然而并沒有一個(gè)對(duì)應(yīng)的getter方法叫g(shù)etImageResourceId(),這樣你是沒有辦法測(cè)試這個(gè)ImageView是不是顯示了你想要的image公般。而在Robolectric實(shí)現(xiàn)的ShadowImageView里面万搔,則提供了getImageResourceId()這個(gè)接口胡桨,你可以用來測(cè)試它是否正確的顯示了你想要的image。

Robolectric入門

build.gradle配置:
dependencies {
    testCompile "org.robolectric:robolectric:3.3.2"
}
注解配置:
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 23)
public class ExampleRobolectricTestCase {
    ......
}

說明:上面配置的是RobolectricTestRunner瞬雹,而不是RobolectricGradleTestRunner昧谊,在Robolectric之前的版本是有這個(gè)RobolectricGradleTestRunner,但在最新的版本上卻沒有了酗捌,也不知道是為什么呢诬。但是有一點(diǎn),使用最新版本后胖缤,倒是沒有出現(xiàn)找不到資源文件res的警告尚镰。最新的Robolectric最高可支持Android API 23。

Android Studio環(huán)境配置:

1.在Build Variants面板中哪廓,將Test Artifact切換成Unit Tests模式狗唉,不過在新版本的Android Studio已經(jīng)不需要做這項(xiàng)配置,如下圖:

Test Artifact.png

2.Working directory設(shè)置
如果在運(yùn)行測(cè)試方法過程中遇見如下異常:

java.io.FileNotFoundException: build\intermediates\bundles\debug\AndroidManifest.xml
......

或者如下警告:

No such manifest file: build/intermediates/bundles/debug/AndroidManifest.xml
......

解決的方式就是將Working directory的值設(shè)置為$MODULE_DIR$涡真。

第一步設(shè)置如下:


Edit Configurations.png

第二步設(shè)置如下:


Run/Debug Configurations.png

設(shè)置完畢后分俯,再次run就可以了。

Robolectric實(shí)戰(zhàn)

首先在build.gradle中的完整配置如下:
    testCompile "junit:junit:4.12"
    testCompile "org.assertj:assertj-core:1.7.0"
    testCompile "org.robolectric:robolectric:3.3.2"

    // PowerMock brings in the mockito dependency
    testCompile 'org.powermock:powermock-module-junit4:1.6.5'
    testCompile 'org.powermock:powermock-module-junit4-rule:1.6.5'
    testCompile 'org.powermock:powermock-api-mockito:1.6.5'
    testCompile 'org.powermock:powermock-classloading-xstream:1.6.5'

從配置中哆料,可以看出在實(shí)際運(yùn)用中缸剪,我們是使用JUnit4+Mockito+PowerMockito+Robolectric,這是一個(gè)牛逼的組合剧劝,在寫單元測(cè)試用例時(shí)簡(jiǎn)直溜得飛起橄登,通過PowerMockito彌補(bǔ)Mockito測(cè)試框架不能mock靜態(tài)方法、final方法和private方法的不足讥此,還可以在JVM中就可以很方便的調(diào)用Android相關(guān)的類和方法拢锹,速度也比較快。

然后定義抽象類BaseRobolectricTestCase:
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowLog.class}, constants = BuildConfig.class, sdk = 23)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.json.*", "sun.security.*", "javax.net.*"})
public abstract class BaseRobolectricTestCase {

    @Rule
    public PowerMockRule rule = new PowerMockRule();

    private static boolean hasInited = false;

    @Before
    public void setUp() {
        ShadowLog.stream = System.out;
        if (!hasInited) {
            initRxJava();
            hasInited = true;
        }
        MockitoAnnotations.initMocks(this);
    }

    public Application getApplication() {
        return RuntimeEnvironment.application;
    }

    public Context getContext() {
        return RuntimeEnvironment.application;
    }

    private void initRxJava() {

        RxJavaPlugins.getInstance().registerSchedulersHook(new RxJavaSchedulersHook() {
            @Override
            public Scheduler getIOScheduler() {
                return Schedulers.immediate();
            }
        });
        RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
            @Override
            public Scheduler getMainThreadScheduler() {
                return Schedulers.immediate();
            }
        });
    }

}

這個(gè)抽象類代碼比較多萄喳,主要是設(shè)置Robolectric單元測(cè)試的運(yùn)行環(huán)境卒稳,方便在單元測(cè)試用例代碼中進(jìn)行復(fù)用。具體分下一下:

  1. @RunWith(RobolectricTestRunner.class)通過注解定義Robolectric運(yùn)行的TestRunner他巨;
  2. @Config(shadows = {ShadowLog.class}, constants = BuildConfig.class, sdk = 23)通過配置shadows = {ShadowLog.class}ShadowLog.stream = System.out;來設(shè)置Android log輸出方式充坑,使得單元測(cè)試運(yùn)行時(shí)在控制臺(tái)中可以看到Android代碼中打印出的log日志;
  3. @PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.json.*", "sun.security.*", "javax.net.*"})通過PowerMockIgnore注解定義所忽略的package路勁染突,防止所定義的package路徑下的class類被PowerMockito測(cè)試框架mock捻爷;
  4. 在setUp()方法中調(diào)用MockitoAnnotations.initMocks(this);初始化PowerMockito注解,為@PrepareForTest(YourStaticClass.class)注解提供支持份企;
  5. 在代碼中也榄,我們可以看到定義了兩個(gè)基本方法getApplication()和getContext(),在寫測(cè)試代碼中使用起來很方便司志,就像在Activity一樣甜紫,增加測(cè)試的可讀性降宅;
  6. 如果項(xiàng)目中使用了rxjava框架,在對(duì)rxjava相關(guān)的代碼進(jìn)行單元測(cè)試時(shí)囚霸,通過initRxJava()方法將異步處理轉(zhuǎn)化為同步處理腰根,如此一來方便單元測(cè)試驗(yàn)證;
最后編寫Activity測(cè)試用例代碼:
public class ComplaintActivityTest extends BaseRobolectricTestCase {

    @Test
    @PrepareForTest({AppUtil.class, OAuthManager.class, NetUtil.class})
    public void jumpCompensate() throws Exception {
        PowerMockito.mockStatic(AppUtil.class);
        PowerMockito.when(AppUtil.getVersionName()).thenReturn("1.4.0");

        PowerMockito.mockStatic(OAuthManager.class);
        OAuthManager mockOAuth = PowerMockito.mock(OAuthManager.class);
        PowerMockito.when(OAuthManager.getInstance()).thenReturn(mockOAuth);
        PowerMockito.when(mockOAuth.getSargerasToken()).thenReturn("c97faa92-34ea-4248-a19e-9a9fb848b29b");

        AppApplication.mInstance = getApplication();

        PowerMockito.mockStatic(NetUtil.class);
        PowerMockito.when(NetUtil.isNetworkConnected(AppApplication.getInstance())).thenReturn(true);

        PreferenceUtil.init();
        PersistentPreferenceUtil.init();

        ComplaintActivity complaintActivity = Robolectric.buildActivity(ComplaintActivity.class).create().get();
        assertNotNull(complaintActivity);
        complaintActivity.jumpCompensate();
        Intent expectedIntent = new Intent(complaintActivity, HelpActivity.class);
        ShadowActivity shadowActivity = Shadows.shadowOf(complaintActivity);
        Intent actualIntent = shadowActivity.getNextStartedActivity();
        Assert.assertEquals(expectedIntent.getComponent().getClassName(), actualIntent.getComponent().getClassName());
    }

}

上面前一部分代碼主要設(shè)置ComplaintActivity運(yùn)行所依賴的屬性拓型,這也是在單元測(cè)試最為繁瑣的地方额嘿,因?yàn)椴皇沁\(yùn)行在真實(shí)的Android環(huán)境中。具體分析如下:

  1. 通過注解@PrepareForTest({AppUtil.class, OAuthManager.class, NetUtil.class})定義PowerMockito要mock的類吨述;
  2. 在Robolectric中讀取不到apk的版本號(hào)岩睁,通過PowerMockito.when(AppUtil.getVersionName()).thenReturn("1.4.0");mock指定AppUtil.getVersionName()的返回值"1.4.0",即版本號(hào)揣云;
  3. 通過AppApplication.mInstance = getApplication();使用Robolectric運(yùn)行環(huán)境中的application對(duì)AppApplication.mInstance進(jìn)行依賴注入,因?yàn)樵诤芏囝愔卸紩?huì)用到AppApplication.mInstance進(jìn)行初始化冰啃,例如SharedPreference邓夕、SQlite、單例類等阎毅,
PreferenceUtil.init();
PersistentPreferenceUtil.init();

上面代碼就需要依賴AppApplication.mInstance進(jìn)行初始化焚刚;

  1. ComplaintActivity complaintActivity = Robolectric.buildActivity(ComplaintActivity.class).create().get();使用Robolectric創(chuàng)建ComplaintActivity對(duì)象,其中create()方法就是對(duì)應(yīng)于調(diào)用Activity生命周期的onCreate()方法扇调,此外Robolectric支持鏈?zhǔn)秸{(diào)用如:Robolectric.buildActivity(ComplaintActivity.class).create().resume().get();矿咕;
  2. assertNotNull(complaintActivity);驗(yàn)證complaintActivity是否跑起來;
  3. 最后一部分代碼就是調(diào)用jumpCompensate方法進(jìn)行跳轉(zhuǎn)狼钮,驗(yàn)證跳轉(zhuǎn)的Intent是否符合預(yù)期碳柱;

至于其他的一些如Fragment、Dialog熬芜、Toast等驗(yàn)證莲镣,可以參考這篇博客,這里就不展開涎拉。

Robolectric常見的坑

1.Application空指針問題

這是因?yàn)镾haredPreferences和單例等類初始化時(shí)需要依賴Application對(duì)象瑞侮,我們常見的用法是使用Application.getApplication()方法來獲取,在Robolectric中則是需要使用RuntimeEnvironment.application來進(jìn)行替換鼓拧,上面就是通過依賴的方式進(jìn)行替換半火。

2. AppCompatActivity錯(cuò)誤

假如你在Robolectric的@Config注解中配置了manifest = Config.NONE,那就完蛋了季俩,因?yàn)樵诰W(wǎng)上根本找不解決的方法钮糖,你遇到如下異常不能使用support V7包的類:

java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.

     at android.support.v7.app.AppCompatDelegateImplV7.createSubDecor(AppCompatDelegateImplV7.java:343)
     at android.support.v7.app.AppCompatDelegateImplV7.ensureSubDecor(AppCompatDelegateImplV7.java:312)
     at android.support.v7.app.AppCompatDelegateImplV7.initWindowDecorActionBar(AppCompatDelegateImplV7.java:172)
     at android.support.v7.app.AppCompatDelegateImplBase.getSupportActionBar(AppCompatDelegateImplBase.java:88)
     at android.support.v7.app.AppCompatActivity.getSupportActionBar(AppCompatActivity.java:110)
     at me.ele.shopcenter.components.BaseActivity.initActionBar(BaseActivity.java:104)
     at me.ele.shopcenter.components.BaseActivity.onCreate(BaseActivity.java:52)
     at me.ele.shopcenter.ui.order.ComplaintActivity.onCreate(ComplaintActivity.java:93)
     at android.app.Activity.performCreate(Activity.java:6251)
     at org.robolectric.util.ReflectionHelpers.callInstanceMethod(ReflectionHelpers.java:231)

解決的方式就是去掉manifest = Config.NONE配置,這是坑爹的种玛,我就遇到這個(gè)錯(cuò)誤藐鹤,花了好長(zhǎng)一段時(shí)間才發(fā)現(xiàn)是這個(gè)配置導(dǎo)致的瓤檐。

3.Asset文件路徑錯(cuò)誤

需要用到context.getAssets().open("XXX")加載asset目錄下的文件時(shí),要是遇到以下錯(cuò)誤:

java.io.FileNotFoundException: build/intermediates/bundles/debug/assets/https.cer (No such file or directory)
    at java.io.FileInputStream.open0(Native Method)
    at java.io.FileInputStream.open(FileInputStream.java:195)
    at java.io.FileInputStream.<init>(FileInputStream.java:138)
    at org.robolectric.res.FileFsFile.getInputStream(FileFsFile.java:84)
    at org.robolectric.shadows.ShadowAssetManager.open(ShadowAssetManager.java:319)
    at android.content.res.AssetManager.open(AssetManager.java)

解決方式是娱节,不要用AssetManager來加載文件挠蛉,而是自己使用Java API來加載文件,如:

new FileInputStream(new File("/Users/michaelzhong/Desktop/shop/talaris_shop_center/app/src/main/assets/https.cer"));

這個(gè)方式有點(diǎn)丑肄满,需要用到你要加載的文件的絕對(duì)路徑谴古,靈活性低,不方便移植稠歉,不過這是我目前想到的解決方式掰担。

4.找不到android.net.http.AndroidHttpClient的類文件

在Android API23開始,google就移除了HttpClient相關(guān)的類怒炸,有兩種方法解決上述問題带饱。
方法一:在build.gradle添加應(yīng)用useLibrary ‘org.apache.http.legacy’
方法二:在test目錄下添加HttpClient類(記得包名為android.net.http),如下:


AndroidHttpClient.png

說明:推薦使用第二種方式阅羹,第二種方法正式打包并不會(huì)把HttpClient的類加入勺疼,減少了包中無用的資源。

小結(jié)

在實(shí)際的使用中捏鱼,Robolectric需要踩很多坑的执庐,不過貴在嘗試。至此导梆,單元測(cè)試系列博客已經(jīng)完結(jié)轨淌,主要分了四篇博客來講述。非常感謝您對(duì)本篇博客的支持看尼,要是有什么不足歡迎指正递鹉!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市狡忙,隨后出現(xiàn)的幾起案子梳虽,更是在濱河造成了極大的恐慌,老刑警劉巖灾茁,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窜觉,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡北专,警方通過查閱死者的電腦和手機(jī)禀挫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拓颓,“玉大人语婴,你說我怎么就攤上這事。” “怎么了砰左?”我有些...
    開封第一講書人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵匿醒,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我缠导,道長(zhǎng)廉羔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任僻造,我火速辦了婚禮憋他,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘髓削。我一直安慰自己竹挡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開白布立膛。 她就那樣靜靜地躺著揪罕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪旧巾。 梳的紋絲不亂的頭發(fā)上耸序,一...
    開封第一講書人閱讀 49,950評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音鲁猩,去河邊找鬼。 笑死罢坝,一個(gè)胖子當(dāng)著我的面吹牛廓握,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播嘁酿,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼隙券,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了闹司?” 一聲冷哼從身側(cè)響起娱仔,我...
    開封第一講書人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎游桩,沒想到半個(gè)月后牲迫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡借卧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年盹憎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铐刘。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡陪每,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情檩禾,我是刑警寧澤挂签,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站盼产,受9級(jí)特大地震影響饵婆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜辆飘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一啦辐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蜈项,春花似錦芹关、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至跑芳,卻和暖如春轴总,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背博个。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工怀樟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人盆佣。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓往堡,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親共耍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子虑灰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,846評(píng)論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)痹兜,斷路器穆咐,智...
    卡卡羅2017閱讀 134,637評(píng)論 18 139
  • 背景 Mock、PowerMock字旭、Junit等都只是在java層面的單元測(cè)試对湃。但對(duì)于android app開發(fā)來...
    johnnycmj閱讀 2,939評(píng)論 1 2
  • 我是可諾媽,養(yǎng)育一對(duì)可愛雙胞胎兄妹谐算,這本書從懷孕開始就讀了熟尉,斷斷續(xù)續(xù)到今天兩娃娃2周歲,當(dāng)雙胞胎兄妹出生頭一年洲脂,我...
    繪愛正面管教閱讀 2,344評(píng)論 1 2
  • 一:簡(jiǎn)介 Retrofit是Square公司開發(fā)的一款針對(duì)Android網(wǎng)絡(luò)請(qǐng)求的框架斤儿,Retrofit2底層基于...
    往事一塊六毛八閱讀 529評(píng)論 0 0