2015年11月28日 星期六

Android 開發(一百零五) 架構建立,利用retrofit & rxjava 來將資料與UI分離

由於最近有點空閒,所以花了點時間看了一下retrofit & rxjava 搭配起來可以做到什麼成果

在講解之前,不知道大家有沒有一樣的經驗,在寫project時,api的code總是跟UI code綁在一起,造成程式邏輯上很複雜,舉個例子來說,例如 https://api.github.com/repos/square/retrofit/contributors 這隻api ,我希望針對回來的json,filter 出login field為JakeWharton的人,以及contributions field大於10的人,並且分別在UI上做相對應的顯示.

可想而知,作法就會是

  1. 取得json List
  2. 針對List filter 出login field 為JakeWharton的物件
  3. 針對List filter 出contributions 大於 10的物件
  4. 顯示到UI上

從code上面看起來就會

List<GithubJson> list = api.getGithubJson();
List<GithubJson> JakeWhartonList = dofilterEquals(list,   "login", "JakeWharton");
loginListCustomView.setData(JakeWhartonList);
List<GithubJson> OverTenList = dofilterOver(list, "contributions", 10);
contributionsListCustomView.setData(JakeWhartonList);

這樣看起來似乎很漂亮…..那是因為這只是個範例,實際上的code應該會比上面這段code複雜好幾倍,所以這也就讓我們開始思考是否有更漂亮的解決方案.

上面這段code最主要的問題在於,資料,處理資料以及UI顯示的程式碼被混雜在一起,造成之後維護上的麻煩,而且重點是無法測試,如果將上面的code寫在activity or fragment 裡,就會發現當ui 呈現錯誤時,無法知道到底是ui錯誤,資料錯誤或是filter錯誤.

所以為了達到我們想要的目標可測試的程式首先就必須先把ui & 資料分離,目標

MyAppClient apiclient = new MyAppClient();
    apiclient.getFilterAndOverTen(new MyAppClient.ICallBack() {
        @Override
        public void callBack(MyWrapper o) {
            //use MyWrapper to updateUI;
        }
    });

為了達到這個目標,我們可以利用rxjava的特性

SimpleService.GitHub github = retrofit.create(SimpleService.GitHub.class);

Observable<List<SimpleService.Contributor>> call = github.contributors("square", "retrofit");

取得observable之後也就是我們想要取得的資料,接著針對資料做filter的動作

       Observable<List<SimpleService.Contributor>> a = call.observeOn(Schedulers.io())
            .subscribeOn(Schedulers.newThread())
            .flatMap(new Func1<List<SimpleService.Contributor>, Observable<SimpleService.Contributor>>() {
                @Override
                public Observable<SimpleService.Contributor> call(List<SimpleService.Contributor> contributors) {
                    return Observable.from(contributors);
                }
            })
            .filter(new Func1<SimpleService.Contributor, Boolean>() {
                @Override
                public Boolean call(SimpleService.Contributor contributor) {
                    return contributor.login.contains("JakeWharton");
                }
            }).toList()
            .doOnNext(new Action1<List<SimpleService.Contributor>>() {
                @Override
                public void call(List<SimpleService.Contributor> contributors) {
                    Log.d("Ted", "size " + contributors.size());
                }
            });

首先我們要先filter出login field 為jakewharton 的人,所以我們必須針對list去做filter,花了一點時間做研究後,發現必須將

Observable<List<SimpleService.Contributor>> 轉為 Observable<SimpleService.Contributor>

filter的動作才可以正確執行
上面的程式碼就是做這樣的事情,接著另一部分的filter當然也是依樣畫葫蘆,當兩個東西都filter完成後,接著重點就是將兩個filter好的東西合併在一起

Observable.zip(a, b, new Func2<List<SimpleService.Contributor>, List<SimpleService.Contributor>, MyWrapper>() {
        @Override
        public MyWrapper call(List<SimpleService.Contributor> contributors, List<SimpleService.Contributor> contributors2) {
            return new MyWrapper(contributors, contributors2);
        }
    }).subscribe(new Action1<MyWrapper>() {
        @Override
        public void call(MyWrapper o) {
            Log.d("TEd","go");
            callBack.callBack(o);
        }
    });

可以注意到,zip就是將兩個物件合起來,然後合成MyWrapper,回傳給外層呼叫的fragment or activity,這樣就完成了

之後如果ui有所改變,data還是可以重用,如果data有所改變,修改的程式也只會限定在data那包裡面,封裝的效果就出來了,當然需要測試的話也可以輕鬆地做測試了.

最後,還是要依照慣例附上sample囉 https://github.com/nightbear1009/retrofit_rxjava_architech

2015年9月20日 星期日

Trunk Based Developement 概述

Trunk Based Development

最近看到了google 的一些文章,裡面提到了google & facebook 都使用了Trunk Based Development,覺得很有趣,所有就去查了一些相關的文章
Trunk Based Development最特別的地方就是,新的功能並不是開一個新的feature branch做開發,而是直接在trunk 上做開發,當然依然要遵守,每筆commit都不能太過於龐大的守則
由於Trunk Based Development 會使多個功能同時在同一個branch上開發,這也就衍生了另一個問題,假設某些功能是v1.0要進,某些功能是v1.1才要進,那要怎麼處理?
根據martin fowler的說法,我們必須實作feature toogle,講白了點就是類似開關的功能,根據設定檔開啟或關閉該功能,舉個例子來說,某個版本要進v1.1,但是我們的功能在v0.1 的時候就已經開始開發,所以我們必須實作一個開關,避免v1.0的版本會出現這個不應該出現的功能,接著我們可以在v1.1的時候將開關打開,然後在v1.2的時候將開關這個功能拔除並且移除舊的功能.
又或者,我們可以實作branch by abstraction的方法,其實也是一個開關的概念或者是說switch的概念,當沒有切換的時候就使用舊版的作法,當切換的時候則改成新版的做法,至於為什麼要使用abstraction的關鍵字?
其實這也對應到了 OO的概念
針對介面來撰寫程式,而不要針對實作
所以在實作這個功能的時候,首先必須先將共同的邏輯抽離抽成interface or abstract,然後舊版則實作這個interface or abstract,新版也實作這個interface or abstract,然後最重要的是使用者只針對interface or abstract 做動作,然後我們就可以去做switch的動作

TBD的好處

  • 降低test的成本
    • 如果有多個feature branch也就代表有多個branch要執行test,這相對也提升了test 成本
  • always是可以release 的codebase,並且可以根據需求,修改需要release的功能
    • 可以利用開關開啟或關閉某項功能
    • 可以利用開關使用新版或舊版功能
  • 減少merge conflict的機會
    • 由於所有人都在同一個trunk上開發,只要所有的人都保證commit的code並不是過於龐大,就算遇到conflict也可以快速解決
    • 適合多人同時開發的專案

結論

講了那麼多,其實我也還沒有機會實際run過這個流程,不過根據目前的理解,TBD講白了就是把所有的功能都放在同一個branch上開發,然後使用featuretoggle & branch by abstraction來隔開尚未完成的實作,很合理…但是同時也覺得會有很多坑啊!!

reference

http://www.alwaysagileconsulting.com/organisation-pattern-trunk-based-development/
http://martinfowler.com/bliki/FeatureBranch.html
http://www.martinfowler.com/bliki/FeatureToggle.html
http://paulhammant.com/blog/branch_by_abstraction.html

2015年9月15日 星期二

MarkDown

其實這篇文章是想要稍微推薦一下markdown
markdown其實是個很輕量的語法,由於用google blog的介面寫blog真的很麻煩

  1. 沒有code block
  2. 排版十分不易
  3. 沒辦法即時預覽

這些使用markdown語法 + stackedit 之後所有的問題都迎刃而解啊!!
我想應該會使用這個editor寫一陣子吧!!

順便熟悉一下新editor的用法

Android 開發(一百零四) what's new in andorid studio 1.4 preview

先看一下下面這張圖


這代表什麼?
代表或許在android studio 1.4 正式release 之後,或許就可以支援
使用vector 產生圖檔了!!

有興趣的人可以先去下載新版的android studio 1.4 beta
然後將build.gradle的 gradle版本改成1.4.0-beta1

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.4.0-beta1'
        // NOTE: Do not place your application dependencies here; they belong        // in the individual module build.gradle files    }
}

然後就可以使用這項功能了,記得在import圖檔之後要sync一下,接著檔案就會產生了
這對UI真的是一大福音啊!!,之後只要出一張vector就好了!!

不過這項功能目前還沒發佈,可以再期待一下囉!!

Android 開發(一百零三) dex-method-counts

今天要講的是app 的method counts

為什麼我們要注意method counts?
因為android 有65536的method count的限制

如果app的method數超過了65536就會無法build成功,
所以為了提早發現提早治療,所以我們必須常常觀察method數是否超出預期

從前並沒有方便的工具去觀察method數,不過最近觀察到github上有人提供了簡單的檢驗方式 https://github.com/mihaip/dex-method-counts

小弟依照了上面的方式稍微檢驗了我們家的app

其實已經快爆了XD

不過如果你檢驗了app後發現遇到跟我類似的情形的話,其實不太需要擔心
因為google 已經想到了這個,所以他提出了 https://developer.android.com/tools/building/multidex.html

multidex,只要利用multidex就可以解決掉這個問題

根據google 的文件我們首先必須import multidex的lib

dependencies {
  compile 'com.android.support:multidex:1.0.0'
}
接著必須在build.gradle裡加上multidexEnabled

    defaultConfig {
        
        multiDexEnabled true

    }
我們必須將原本的application改成繼承MultiDexApplication

public class MyApp extends MultiDexApplication  {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }

這樣就完成了

不過注意一下,multidex還是有一些限制的,
例如必須要在版本14以上,還有可能會造成build的速度變慢之類的問題

最後,如果有寫test case 的人,而且有用AndroidJUnitRunner的話
可能會想問要怎麼在multidex版本上面正常測試

方法很簡單,首先必須在test的folder裡自建一個runner
這個runner必須必得要使用multidex.install

public class MyRunner extends AndroidJUnitRunner {

    @Override
    public void onCreate(Bundle arguments) {
        MultiDex.install(getTargetContext());
        super.onCreate(arguments);
    }
}

接著必須在build.gradle裡加上testInstrumentationRunner

    defaultConfig {
        minSdkVersion 14
        targetSdkVersion 22

        testInstrumentationRunner "com.MyRunner"
        multiDexEnabled true

    }

這樣就完成了,就可以使用舊有的annotation了







2015年6月13日 星期六

Android 開發(一百零二) 利用annotation 處理 enum

在java 中我們常常會使用enum處理多種type,不過android 官方說這種寫法是非常expensive (可能是效率或memory其實我沒有多做研究)所以官方推薦我們利用另外一種寫法,使用annotation來處理

先讓我們看一下下面的程式碼

public class MyMode {
    @IntDef({SUCCESS, NETWORK_FAIL, DATA_ERROR})
    @Retention(RetentionPolicy.SOURCE)
    public @interface APIMode {}

    public static final int SUCCESS = 0;
    public static final int NETWORK_FAIL = 1;
    public static final int DATA_ERROR = 2;

    private int mode = SUCCESS;
    public int getMode(){
        return mode;
    }

    public void setMode(@ APIMode int _mode){
        mode = _mode;
    }

}

我定義了一個MyMode 並且定義了
SUCCESS = 0
NETWORK_FAIL = 1
DATA_ERROR = 2

接著我利用了
@IntDef({SUCCESS, NETWORK_FAIL, DATA_ERROR})
    @Retention(RetentionPolicy.SOURCE)
    public @interface APIMode {}

定義了一個自定義的annotation APIMode 而這個annotation只有在setMode的時候會被用到,這個annotation有什麼功用呢?

主要是在寫程式的時候,我們常常會這樣寫 mymode.setMode(1);
這樣寫程式是可以work的,但是其實很不好讀,對於其他的developer他們還必須進來查看才知道原來setMode(1)是在說 NETWORK_FAIL ,
所以我們該如何避免developer為了方便,而使用這種寫法呢?

我們使用了剛剛的annotation時,如果有人寫同樣的code,我們就可以在android studio上面看到如下圖的提示

而且數字下方會有紅字顯示,這樣developer就比較會去修改它,可惜的是,這種寫法似乎沒有辦法強制build不過(或許使用lint就行)

最後,我覺得這種寫法的確是蠻不錯的,but對於實際在使用上面,感覺大部份的人還是會為了方便而使用enum,不過官方都這麼推薦了,還是可以考慮用看看囉~

Android 開發(一百零一) google play Service Invite friend Api

google play service 在新版本推出了invite friend api ,讓你可以邀請google 上的朋友,
做法其實很簡單,

        Intent intent = new AppInviteInvitation.IntentBuilder(getString(R.string.invitation_title))
                .setMessage(getString(R.string.invitation_message))
                .setDeepLink(Uri.parse("http://tw.91mai.com/ref/0"))
                .build();
        startActivityForResult(intent, REQUEST_INVITE);

使用上面的方式將intent丟出,就會開啟邀請好友的選單,在邀請完成後利用onActivityResult去取得邀請的ids

 @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        Log.d(TAG, "onActivityResult: requestCode=" + requestCode + ", resultCode=" + resultCode);

        if (requestCode == REQUEST_INVITE) {
            if (resultCode == RESULT_OK) {
                // Check how many invitations were sent and show message to the user
                // The ids array contains the unique invitation ids for each invitation sent
                // (one for each contact select by the user). You can use these for analytics
                // as the ID will be consistent on the sending and receiving devices.
                String[] ids = AppInviteInvitation.getInvitationIds(resultCode, data);
                showMessage(getString(R.string.sent_invitations_fmt, ids.length));
            } else {
                // Sending failed or it was canceled, show failure message to the user
                showMessage(getString(R.string.send_failed));
            }
        }
    }

當你將邀請送出之後,你的朋友就會收到邀請下載的信,

out16.gif
out16.gif

如上方兩張圖,左邊是送出邀請的功能
右邊是好有收到邀請時,安裝後開啟時我們可以從intent中取得相關的link and id

為了要能夠在安裝時取得google play store送給我們的廣播,我們必須註冊receiver

    <receiver
           android:name="com.google.android.gms.samples.appinvite.ReferrerReceiver"
            android:exported="true"
            tools:ignore="ExportedReceiver">
            <intent-filter>
                <action android:name="com.android.vending.INSTALL_REFERRER" />
            </intent-filter>
        </receiver>

接著我們可以利用下方的程式碼取得相關的資訊

if (!AppInviteReferral.hasReferral(intent)) {
            Log.e(TAG, "Error: Intent does not contain App Invite");
           
// Extract referral information from the
String invitationId = AppInviteReferral.getInvitationId(
String deepLink = AppInviteReferral.getDeepLink(intent);

如果我們不需要track 成效的話,邀請的功能在這邊已經結束了
接下來要講的就是,我們可以利用google play service新的api 來trace我們的成效

在使用朋友贈送的信件開啟我們的app之後,我們可以利用google play service來track成效,

        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .addConnectionCallbacks(this)
                .enableAutoManage(this, 0, this)
                .addApi(AppInvite.API)
                .build();

利用AppInvite.API  在api connect成功之後


  String invitationId = AppInviteReferral.getInvitationId(intent);

        // Note: these  calls return PendingResult(s), so one could also wait to see
        // if this succeeds instead of using fire-and-forget, as is shown here
        if (AppInviteReferral.isOpenedFromPlayStore(intent)) {
            AppInvite.AppInviteApi.updateInvitationOnInstall(mGoogleApiClient, invitationId);
        }

        // If your invitation contains deep link information such as a coupon code, you may
        // want to wait to call `convertInvitation` until the time when the user actually
        // uses the deep link data, rather than immediately upon receipt
        AppInvite.AppInviteApi.convertInvitation(mGoogleApiClient, invitationId);

我們可以取得inviteationId ,然後利用 updateInvitationOnInstall 告訴google analytics這個user已經成功安裝.

假設這個邀請是希望新安裝的user可以使用優惠券,那我們可以利用
convertInvitation告訴google analytics有多少使用者安裝後並且成功使用優惠券
也就是可以取得轉換率(我是這樣理解的XD)



最後,由於我是直接拿google 的sample就直接附上相關的網址吧,
有興趣的人可以去那邊做更深入的了解
https://developers.google.com/app-invites/android/guides/app?configured=true

然後下面的網址是appinvite設定的小幫手,在裡面跟著步驟設定之後,你google developer console的設定也會設定完成,個人覺得這個新界面真的是好用很多,大家可以參考看看
https://developers.google.com/mobile/add?platform=android&cntapi=appinvites&cnturl=https:%2F%2Fdevelopers.google.com%2Fapp-invites%2Fandroid%2Fguides%2Fapp%3Fconfigured%3Dtrue%23add-config&cntlbl=Continue%20Adding%20App%20Invites