2014年4月26日 星期六

Android 開發 (四十) FloatingLabelLayout

FloatingLabelLayout
 https://gist.github.com/chrisbanes/11247418

這是ChrisBanes公布的source code 先看一下功能


我們可以稍微看一下source code,

FloatLableLayout.java


  
    @Override
    public final void addView(View child, int index, ViewGroup.LayoutParams params) {
        if (child instanceof EditText) {
            // If we already have an EditText, throw an exception
            if (mEditText != null) {
                throw new IllegalArgumentException("We already have an EditText, can only have one");
            }

            // Update the layout params so that the EditText is at the bottom, with enough top
            // margin to show the label
            final LayoutParams lp = new LayoutParams(params);
            lp.gravity = Gravity.BOTTOM;
            lp.topMargin = (int) mLabel.getTextSize();
            params = lp;

            setEditText((EditText) child);
        }

        // Carry on adding the View...
        super.addView(child, index, params);
    }
private void setEditText(EditText editText) {
        mEditText = editText;

        // Add a TextWatcher so that we know when the text input has changed
        mEditText.addTextChangedListener(new TextWatcher() {

            @Override
            public void afterTextChanged(Editable s) {
                if (TextUtils.isEmpty(s)) {
                    // The text is empty, so hide the label if it is visible
                    if (mLabel.getVisibility() == View.VISIBLE) {
                        hideLabel();
                    }
                } else {
                    // The text is not empty, so show the label if it is not visible
                    if (mLabel.getVisibility() != View.VISIBLE) {
                        showLabel();
                    }
                }
            }

            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
            }

        });

可以看到他在 addview時將 child的 edittext加上  textchanged Event
然後再利用textChanged event去顯示或隱藏相關的label


其實我比較好奇的地方是他為何不乾脆將 editText 直接包含在custom view裡頭
反而是利用如下圖的方式


    <com.example.my.FloatLabelLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        app:floatLabelTextAppearance="@style/TextAppearance.YourApp.FloatLabel" >

        <EditText
            android:id="@+id/edit_password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="password"
            android:imeOptions="actionDone"
            android:inputType="textNoSuggestions"
            android:singleLine="true" />

    </com.example.my.FloatLabelLayout>

是說這樣寫的自由度比較高,但是使用者錯誤使用的機會也會增加,可以想像的好處大概只有當 FloatLabelLayout 的child 有較多UI時,可以將layout 都放在 floatLabelLayout內可以少使用一個
relative layout 如下


    <com.example.my.FloatLabelLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        app:floatLabelTextAppearance="@style/TextAppearance.YourApp.FloatLabel" >

        <EditText
            android:id="@+id/edit_password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="password"
            android:imeOptions="actionDone"
            android:inputType="textNoSuggestions"
            android:singleLine="true" />

        <ImageView android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_launcher"/>
    </com.example.my.FloatLabelLayout>


但是就如上面所說的,當使用者加入兩個editText將會造成錯誤的情況,我目前能想到比較好的解法應該是將view綁定TextView而不是FrameLayout,
簡單的說就是客製一個TextView 並包含上面的功能。


小結

這算是個簡單卻頗有趣的UI。

2014年4月23日 星期三

設計模式(十二) Bridge 模式

Bridge 模式 :  將抽象與實作解耦合,使他們都可以獨立的變化。

解耦: 讓事物獨立地行事,或者至少明確的宣告之間的關係
抽象:不同事物之間概念上的關聯方式


Bridge模式遵循兩個原則:

  1.  找出變化並封裝之
  2. 優先使用物件聚合而不是類別繼承

舉個例子:
假設我有兩個版本的繪圖程式DP1和DP2然後我希望用這兩個繪圖程式來繪製圖形
方形和圓形,範例如下圖



依照需求來看,我們可能會畫出類似上面的UML圖,
接著開始思考該如何將圖組合在一起,

首先我們從Client使用物件的順序來思考,假設Client需要畫一個V1Rectangle,
依序會呼叫 Client -> rectangle.draw -> dp1.drawline *4 
我們發現雖然類別圖的物件非常的多,但是實際上我們只須處理三個物件
  1. 使用矩形的Client物件
  2. V1Rectangle物件
  3. DP1物件
接著我們可能會畫出如下的UML圖


利用繼承來解決這個需求似乎是很自然的解法,但是再仔細思考一下,如果現在新增了一種形狀和一種繪圖程式,物件的數量會變成9個,如果將繪圖程式持續增加,將會造成物件爆炸,
這讓我們必須重頭思考這樣的設計是否是有問題的。

過度使用繼承
過度使用繼承透過特殊化處理變化,從已有的類別衍生新類別,會造成物件越來越難以維護
用設計模式進行思考,用物件的職責來思考而不是其結構,良好的設計應該為每種變化使用不同的特殊化(繼承)並將變化轉移到使用或擁有這種變化的物件中(組合)

那我們應該怎麼做呢?

我們應該
  1. 找出變化並封裝之
  2. 優先使用物件聚合,而不是類別繼承
例如 Client應該只知道他在使用一個Shap物件而不應該知道是Rectangle或Circle,
Shap應該只知道目前是使用某一個版本的Drawing繪圖程式而不必知道是V1或是V2

再看一次下面的圖


Client只知道Shap 所以我們可以思考出左邊的類別圖,Shap應該只知道Drawing
所以我們會得到右邊的圖,接著兩張圖該怎麼聯結再一起呢?

這是一個抽象與實作分離的例子,形狀的抽象與繪圖實作分離,這其實就是Bridge模式。

讓我們來看一下Bridge模式的特徵

意圖:將一組實作與另一組使用他們的物件分離

問題:一個抽象類別的衍生類別必須使用多個實作,但不能出現類別數量爆炸性增長。

參與者與協作者:Abstraction為要實作的物件定義介面,Implementor為具體的實作類別定義介面
Abstraction的衍生類別使用Implementor的衍生類別,卻無須知道自己具體使用哪一個ConcreteImplementor

解決方案:為所有實作定義一個介面,供抽象類別的所有衍生類別使用。

效果:實作與使用實作的物件解耦,提供了可擴展性,客戶物件無須操心實作問題。

實作:
  • 將實作封裝在一個抽象類別。
  • 在要實作的抽象基礎類別中包含一個實作(也可利用介面代替抽象類別)。


小結:

Bridge模式把握幾個原則

物件對自己負責: Shap可以繪製自己,Drawing負責繪圖元素

抽象類別:Shap代表形狀,Drawing代表繪圖程式

透過抽象類別進行封裝:Client只看的到Shap , Shap只知道Drawing 

一條規則,實作一次:  抽象類別中經常有些方法實際使用實作物件,抽象類別的子類別會忽叫這些方法,如此再需要修改時,只需要修改一個地方。

可測試性:改進的方案,讓我們可以很輕易的做測試,可分別對shap以及drawing做測試。

2014年4月13日 星期日

設計模式 (十一) 開拓視野 part2

開拓視野 part1 提出了物件,封裝,繼承的一些不同看法,在part2會針對聚合,共通性和可變性分析與抽象類別,以及敏捷程式設計的品質做相關的介紹。

在part1我們可以瞭解到特殊化繼承帶來的便利性,以及之後所造成的麻煩,為了避免這個問題,我們可以使用聚合的方式將需要的功能帶入,卻又可以保持內聚性以及封裝性,

舉個例子
現在有個需求

  • 每種動物都有數量不同的腿
  • 動物物件必須能夠記住並獲取這一資訊
  • 每種動物的移動方式都不同
  • 對於指定的地形類型,動物物件必須能夠計算出往返的時間
接著該怎麼實作呢?
我們發現每種動物的移動方式是不同的,有些動物是會飛,有些動物會跑,有些動物會游泳,我們總不能為了每種動物都使用一個method吧,但是我們發現他們的都有移動的行為,我們發現了變化(移動方式),同時我們也發現了相同的概念(移動的行為),所以我們可以將變化的部分封裝起來,在Design pattern中有提到

考慮設計中哪些地方可能有變化,如何在不重新設計的情況下增加需求因應變化,關鍵在於封裝變化的概念。

經過封裝變化之後我們的uml 會如下所示

這麼做的好處在於,當有新的behavior出現,只需要增加一個class即可,並不會重新設計。

注意到上面在分析如何封裝時其實有一個分析的方式叫做共通性和可變性分析和抽象類別,
共通性分析就是尋找一些共同的要素,例如移動的行為,可變性分析則說明了物件之間不同的行為,例如飛,走,游。

在設計模式的解析與活用中提到

共通性分析尋找的是不可能隨時間而改變的結構,而可變性分析則要找到可能變化的結構,
從架構的視角來看,共通性分析為架構提供長效的要素,而可變性分析則促進適應實際使用所需。



從上面這張圖我們可以看出這樣的概念

  • 抽象類別(共通性):需要用甚麼介面來處理這個類別的所有責任
  • 衍生類別(可變性):對於這個指定的實作(這個變化),應該怎樣根據指定的規約來實作他

下面有關於使用抽象類別進行特殊化的好處,也就是繼承抽象的好處



我們發現這種設計模式,總是預先設計大概的雛形,這是否跟敏捷開發中不預先設想的想法衝突?

關於這個問題我們再進一步仔細思考,我認為在某些情況下我們的確不該預先設想,過於關注細節,我們應該觀察物件發生變化的可能,並且使用封裝的方式將變化封裝,然而我們必須注意的是,敏捷開發以及設計模式所關注的共同要點
  1. 無冗餘
  2. 可讀性
  3. 可測試性
無冗餘性

Kent Back Once and Only Once rule,他說明了功能只在一個地方實作,也就是同樣的程式碼不出現在第二個地方,避免出現重複的程式碼,消除重覆的好處在於,當功能需要更改時我們只需要更改一個地方,而且在更改時我們不需再去考慮是否有遺漏。
這種概念就跟依介面設計的概念不謀而合,找出變化,利用介面使程式碼高度內聚。

可讀性

也就是反應意圖,這如同依介面設計的概念,當我們使用介面時,我們就是利用了介面來傳遞我們的概念,例如  上面動物行為的例子中  behavior.move(),代表著動物執行了移動的動作
但是我們並不在乎動物是如何移動的,它反映了動物的意圖。

可測試性

TDD與依介面設計的概念也是一樣的,如果封裝的好,就會產生高內聚,鬆耦合的程式碼,
也會讓我們在寫測試的時候更加的方便。

其實敏捷開發與設計模式要求的都是

內聚: 程式碼更容易測試,因為程式碼只負責一個責任(SRP)
鬆耦合:由於需要關注的東西較少,所以在測試的時候不需要去在乎太多其他類別的東西
冗餘程式碼:冗餘程式碼過多會造成測試涵蓋率下降
可讀性好:可以更明確的表達意圖
封裝性好:鬆耦合,減少對其他物件的關注

小結

其實不管是設計模式或是敏捷開發,其實大家在乎的要點都是,如何開發出高品質的程式碼,
簡單的說,想辦法寫出高內聚,鬆耦合,封裝變化的程式碼就對了

Robolectric介紹(二) 如何使用Robolectric 開發 project with SherlockActionBar

在 Robolectric介紹(一) shadow 介紹完簡單的功能之後,我開始思考是否有辦法將這個方法套用在我們的Project上面,在設定完成之後我嘗試run了一次test,結果如下


java.lang.IllegalStateException: there must have been some overlap for resourceIdToResName! expected 5815 but got 5814
at org.robolectric.res.MergedResourceIndex.merge(MergedResourceIndex.java:25)
at org.robolectric.res.MergedResourceIndex.(MergedResourceIndex.java:17)
at org.robolectric.res.RoutingResourceLoader.(RoutingResourceLoader.java:22)
at org.robolectric.RobolectricTestRunner.createAppResourceLoader(RobolectricTestRunner.java:598)
at org.robolectric.RobolectricTestRunner.getAppResourceLoader(RobolectricTestRunner.java:582)
at org.robolectric.internal.ParallelUniverse.setUpApplicationState(ParallelUniverse.java:98)
at org.robolectric.RobolectricTestRunner.setUpApplicationState(RobolectricTestRunner.java:401)
at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:219)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:174)
at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)

在我花了很久時間debug與search之後,發現在xml內的小差異會造成test fail android:id="@android:id 以及 android:id="@+id 
只有android要求必須使用@android:id的才使用這種設定方式 例如 tabhost,ListFragment
否則一律使用 android:id="@+id ,錯用這個設定會造成Robolectric 無法 test

在projct成功可以test之後我遇到了另一個問題,由於我們的project implement了
ActionBarSherlock 所以在測試的時候常常會看到一些關於actionbar設定的nullpointer
為了解決這個問題,就必須在測試前預先做一些相關的設定

ActionBarSherlockRobolectric 提供了相關的解法,由於他提供的解法是給舊版本的,所以在這邊我們必須做一些小修改,只需要將

shadowOf(mActivity).setContentView(contentView);

修改成

mActivity.getWindow().setContentView(contentView);

就完成了 

接著我就可以開始做相關的測試 例如我希望測試從mainactivity 導向某個fragment之後的測試 
我可以這樣寫
 @Before
 public void init(){
  ActionBarSherlock.registerImplementation(ActionBarSherlockRobolectric.class);
  ActionBarSherlock.unregisterImplementation(ActionBarSherlockNative.class);
  ActionBarSherlock.unregisterImplementation(ActionBarSherlockCompat.class);
  
 }
 @Test @Config(reportSdk = 10, manifest = "AndroidManifest.xml")
    public void shouldNotBeNull() throws Exception {
  
     ShadowApplication shadowApplication = Robolectric.shadowOf(Robolectric.application);
     shadowApplication.declareActionUnbindable("com.google.android.gms.analytics.service.START");
     Fragment myFragment = new myTestFragment();
       
     
        startFragment( myFragment );

       assertNotNull( myFragment );
    }
    
    public static void startFragment( Fragment fragment )
    {

     SherlockFragmentActivity activity = Robolectric.buildActivity( MainActivity.class )
                                               .create()
                                               .start()
                                               .resume()
                                               .get();   

        FragmentManager fragmentManager = activity.getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.add( fragment, null );
        fragmentTransaction.commit();
    }

需要注意的是,我在init做了相關的設定之後,接著在test的地方做了一些相關的Config,
AndroidManifest是要測試project的AndroidManifest.xml 否則會出現resourceNameNotFound
由於我們的project有使用google trace analytics所以必須加入下面這行
shadowApplication.declareActionUnbindable("com.google.android.gms.analytics.service.START");

否則會出現
java.lang.NullPointerException
 at com.google.analytics.tracking.android.AnalyticsGmsCoreClient$AnalyticsServiceConnection.onServiceConnected(AnalyticsGmsCoreClient.java:176)
 at org.robolectric.shadows.ShadowApplication$2.run(ShadowApplication.java:246)
 at org.robolectric.util.Scheduler$PostedRunnable.run(Scheduler.java:162)
 at org.robolectric.util.Scheduler.runOneTask(Scheduler.java:107)
 at org.robolectric.util.Scheduler.advanceTo(Scheduler.java:92)
 at org.robolectric.util.Scheduler.advanceToLastPostedRunnable(Scheduler.java:68)
 at org.robolectric.util.Scheduler.unPause(Scheduler.java:25)
 at org.robolectric.shadows.ShadowLooper.unPause(ShadowLooper.java:220)
 at org.robolectric.shadows.ShadowLooper.runPaused(ShadowLooper.java:259)
 at org.robolectric.util.ActivityController.create(ActivityController.java:111)
 at org.robolectric.util.ActivityController.create(ActivityController.java:123)
 at MyFragmentTest.startFragment(MyFragmentTest.java:61)
 at MyFragmentTest.shouldNotBeNull(MyFragmentTest.java:50)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
 at java.lang.reflect.Method.invoke(Unknown Source)
 at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
 at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
 at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
 at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
 at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:233)
 at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
 at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:174)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
 at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)


接著myTestFragment 其實是使用mock的原理,由於我測試的Fragment在init的時候會去
onCreateOptionsMenu ,並且執行 mItem.getActionView(), 並且做一些相關的設定
但是我們的actionbar是用mock出來的所以在getActionView的時候會回傳null,
由於Menu的相關功能我可以額外寫一個測試,所以在這裡我並不想測試這部分也不想關心這部分,所以我直接mock了我想測試的fragment並且override了onCreateOptionsMenu的method,範例如下

public class myTestFragment extends MyFragment{

 @Override
 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
  // TODO Auto-generated method stub
  //super.onCreateOptionsMenu(menu, inflater);
 }
}

在相關的設定都設定完成之後,我run了一次test結果卻依舊是紅燈


android.view.InflateException: <merge /> can be used only with a valid ViewGroup root and attachToRoot=true
 at android.view.LayoutInflater.inflate(LayoutInflater.java:455)
 at android.view.LayoutInflater.inflate(LayoutInflater.java:396)
 at android.view.LayoutInflater.inflate(LayoutInflater.java:352)
 at ActionBarSherlockRobolectric.setContentView(ActionBarSherlockRobolectric.java:40)
 at com.actionbarsherlock.app.SherlockFragmentActivity.setContentView(SherlockFragmentActivity.java:261)
 at com.nineyi.MainActivity.onCreate(MainActivity.java:268)
 at android.app.Activity.performCreate(Activity.java:5104)
 at org.fest.reflect.method.Invoker.invoke(Invoker.java:112)
 at org.robolectric.util.ActivityController$1.run(ActivityController.java:116)
 at org.robolectric.shadows.ShadowLooper.runPaused(ShadowLooper.java:257)
 at org.robolectric.util.ActivityController.create(ActivityController.java:111)
 at org.robolectric.util.ActivityController.create(ActivityController.java:123)
 at MyFragmentTest.startFragment(MyFragmentTest.java:61)
 at MyFragmentTest.shouldNotBeNull(MyFragmentTest.java:50)
 at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
 at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
 at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
 at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
 at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:233)
 at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
 at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:174)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
 at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)


這個地方我目前還想不到解法,原因是在MainActivity使用了<merge> 當作我們的root,造成了他的錯誤,由於目前還找不到特別的解法,所以目前這部分我也只能先將source改成 releativeLayout,希望之後有其他的方法可以解決這個問題

在經過了一番折騰努力之後終於看到了期待已久的綠燈...
不過這只不過是個開始,測試的路才剛開通而已~


相關參考文件
https://gist.github.com/JakeWharton/3803294
https://github.com/playhaven/playhaven-robolectric
http://robolectric.blogspot.mx/2013/05/configuring-robolectric-20.html
http://robolectric.blogspot.tw/2013/04/the-test-lifecycle-in-20.html
http://stackoverflow.com/questions/17993239/getsupportactionbar-returns-null-with-robolectric/18034473#18034473

2014年4月10日 星期四

設計模式 (十) 開拓視野 part1

關於物件導向設計有三個基本概念,物件,封裝和抽象類別,設計人員對這些概念的看法是很重要的,傳統的看法有很大的侷限性

傳統的概念

  1. 將物件視為資料和方法的簡單集合
  2. 封裝視為資料隱藏
  3. 繼承 = 特殊化再利用
新的看法

  1. 將物件視為具有責任的東西
  2. 封裝視為隱藏一切的一種能力
  3. 繼承 = 物件分類的一種方法
  4. 共通性與可變性分析
  5. 概念視角,規約視角,實作視角抽象類別與衍生類別的關係
  6. 設計模式與敏捷程式設計方法的差別(冗餘性,可讀性,測試性)

物件

以傳統的方式來看,物件只不過是個資料處理的方法,然而新的看法則是物件是一個具有責任的實體,這兩個差別在於,將物件視為一個責任的實體會讓我們更專注於物件的意圖(責任),而不是如何實作他,我們可以利用這個概念
  1. 做出初步的設計,這個物件的責任為何,(interface abstract)各種抽象的概念
  2. 當我們將一切的職責分配完成再來關心實作部分
舉個例子來說

有個需求希望在畫面上顯示/移除形狀,依照這個需求我們可以想像出一個物件需要負責這些事情
  1. 選擇一種形狀
  2. 顯示
  3. 移除
  4. 取得位置
再進一步仔細想想,是否有發現,所有的形狀物件都一定擁有2,3,4三個必要功能(責任)
所以最後的UML圖會長像是下面的樣子
在此時我們尚未關注細節,我並不曉得方形該怎麼顯示,更不知道三角形該如何取得各點的位置,我不需要瞭解到細節,事實上,這種概念將物件的實作與使用他的物件解耦了。


封裝

傳統的看法為資料的隱藏,然後這種看法侷限性太大了,新的看法則是將封裝視為任何型式的隱藏
  1. 實作細節
  2. 衍生類別
  3. 設計細節
  4. 實體化設計

讓我們看看上面的UML圖,這是一個adapter pattern的UML圖

Shape shape = new Rectangle(); 
  • 這個動作為類的封裝

shape.Display();  
  • 實作的封裝

利用adapter pattern 將 third-party Circle封裝進 Circle  
  • 其他物件的封裝
Rectangle / Triangle/Circle ,詳細的資料只存在自己的class內部,例如一些flag ,各自的location....etc
  • 資料的封裝

用這種更寬廣方式來看待封裝,優點就是能夠帶來一種更容易分解程式的方法,而封裝層就成為了設計需要遵循的介面,透過封裝Shape的衍生類別,當我有新的形狀要加入時只需要新增一個class就好了,並不會對其他的程式造成影響。

早期的提倡者,曾將類別得再利用作為巨大的優勢,通常都是建立一個基礎類別之後,然後再從基礎類別衍生出新的特殊化的類別,如下圖


可以想成是普通的圓形以及以虛線畫成的特殊圓形,
利用繼承的好處是我可以快速的利用Circle已經實作的程式碼,但是...........
壞處呢?

  • 繼承造成了弱內聚,我再修改Circle的程式碼時,我還必須關注SpecialCircle是否會出現SideEffect。
  • 假設我今天希望畫一個虛線版本的三角形呢? 畫虛線的程式碼是否無法再利用,我必須重新建立一個新的物件叫做SpecialTriangle,然後裡頭再重新實作虛線的功能,這種作法減少了程式碼的再利用性
  • 同上面描述的,這也減少了物件的延展性,假設今天有更多更多的類別出現,粗線圓形,紅色圓形,漸層圓形,你會發現程式越來越難改,if else判斷越加越多,bug越來越多?!
而另一種方式則是依相同行為分類,使用聚合,關於這部分會在part2的時候做更多的說明~

2014年4月8日 星期二

設計模式 (九) adapter pattern

adapter pattern又稱轉換器,介面轉換。

adapter pattern 就是將一個類別的介面轉換成客戶希望的另一個介面,也就是讓原本不相容的介面一起工作。

舉例來說,假設有以下的需求

  1. 我希望每個shape類別都有顯示的行為
  2. 使用者不需要知道物件為甚麼形狀
接著大概會出現類似下面的程式碼

public abstract class Shape {

	public abstract void Show();
}

class Rectangle extends Shape
{

	@Override
	public void Show() {
		System.out.println("Rectangle");
	}
	
}

class Triangle extends Shape
{

	@Override
	public void Show() {
		System.out.println("Triangle");
	}
}

UML圖會長這樣

可以很看出,Rectangle和Triangle繼承並實作了Shape

接著在此時又出現了一個需求,需要再多實作一個五芒星的圖型
可是我並不知道如何實作這個功能,所以請教了google 大神之後,我得到了一個第三方的sdk
他的功能很完整,不過實作方法有點不同,如下

public class FiveStar
{
	public void FiveStarDisplay()
	{
		System.out.println("FiveStar");
	}
}

可以發現到,我現在必須使用if else 來判斷該怎麼顯示圖型

	public static void showShap(Object shape)
	{
		if(shape instanceof Shape){
			((Shape)shape).Show();
		}
		else if(shape instanceof FiveStar){
			((FiveStar)shape).FiveStarDisplay();
		}
	}
這是個悲劇的開始,客戶必須區分這是甚麼形狀,只要用到shape就必須多加if else 判斷,
並且或許在未來某天,還可能誤將 shape 轉型轉錯造成app crash。

那麼我們可以做甚麼來避免這場悲劇的發生呢?

先思考一個問題,怎麼做對我們最好?
如果我們有辦法將FiveStar視為Shape那是不是所有的問題都迎刃而解了呢?
可是該怎麼做呢?

看看下面的方式

class FiveStarAdapter extends Shape
{
	private FiveStar star;
	@Override
	public void Show() {
		// TODO Auto-generated method stub
		star.FiveStarDisplay();
	}
	
}

我們將 FiveStar封裝在 FiveStarAdapter裡這樣既達到了實作五芒星的功能,又可以維持客戶不需要區分是甚麼形狀就可以使用,接著就可以把showShap改成這樣


	public static void showShap(Shape shape)
	{
			shape.Show();
	}


程式變得更加簡潔,而且更低耦合度了


結論
Adapter的實作方式就是建立一個具備所需介面的新類別(FiveStarAdapter),然後包裝原有的類別(FiveStar),如此就可以將原有的類別轉型成Shape

2014年4月6日 星期日

設計模式 (八) facade pattern

Facade Pattern

定義一個更高層的介面,使子系統更加容易使用,提供更簡單的方法與系統交流

  • 意圖:希望簡化原有系統的使用方式
  • 問題:只需要使用某個複雜系統的子集,或者需要以一種特殊的方式與系統交流
  • 解決方案:façade為原有系統的客戶提供了一個新的介面
  • 參與者與協作者:為客戶提供一個簡化介面,更容易使用
  • 效果:façade模式簡化了對所需子系統的使用過程,由於façade並不提供完整的功能,客戶可能無法使用某些功能
  • 實作:定義一個或多個具備所需介面的新類別;讓新的類別使用原有的系統

下面有四個class 分別是家庭Facade控制器,風扇控制器,燈光控制器,電視控制器

public class HomeDeviceFacade {
 private FanController FC = new FanController();
 private LightController LC = new LightController();
 private TVController TC = new TVController();
 
 public void DeviceOn()
 {
  FC.TurnOn();
  LC.TurnOn();
  TC.TurnOn();
 }
 
 public void DeviceOff()
 {
  FC.TurnOff();
  LC.TurnOff();
  TC.TurnOff();
 }

 public static void main(String args[]) {
    
 System.out.println("====Facade====");
     HomeDeviceFacade facade = new HomeDeviceFacade();

        facade.DeviceOn();
        System.out.println();
        facade.DeviceOff();          
    }
}

class FanController
{
 private int speed;
 public void ChangeFanSpeed(int _speed)
 {
  speed = _speed;
 }
 public void TurnOn()
 {
  System.out.println("Fan on");
 }
 
 public void TurnOff()
 {
  System.out.println("Fan off");
 }
}

class LightController
{ 
 public void TurnOn()
 {
  System.out.println("Light on");
 }
 
 public void TurnOff()
 {
  System.out.println("Light off");
 }
}

class TVController
{
 private int vol;
 public void changeVolume(int _vol)
 {
  vol = _vol;
 }
 public void TurnOn()
 {
  System.out.println("TV on");
 }
 
 public void TurnOff()
 {
  System.out.println("TV off");
 }
}


可以看出Facade讓我們簡化了其他的動作,原本需要一個一個將電視風扇燈光打開,現在只需要一個步驟就可以將所有的裝置做開關,這簡化了系統

結論
Facade模式提出了一種通用方法,建立了新介面供客戶使用, 客戶並不需要原有系統的所有功能

Facade還有其他功用
  • 追蹤系統的使用情況
由於所有的裝置都會經過HomeDeviceFacade 所以我們可以利用這個class 追蹤裝置的使用狀況 
  • 改換系統
由於裝置都經由HomeDeviceFacade 做處理,當有新版本的 TVController2 出現時,我們的控制程式(Main)並不需要做更改,只需要修改HomeDeviceFacade  裡的 TVController即可。

最後依照慣例附上 Sample Code

2014年4月2日 星期三

Android 開發 (三十九) facebook send app request

什麼是app request ?

如下圖我們可以透過app寄送邀請給使用者

該怎麼寄送?

使用facebook 內建的sdk WebDialog來傳送資料


 WebDialog.RequestsDialogBuilder builder =
     new WebDialog.RequestsDialogBuilder(mActivity, Session.getActiveSession())
             .setOnCompleteListener(new WebDialog.OnCompleteListener() {
                 @Override
                 public void onComplete(Bundle values, FacebookException error) {
                     if (error != null) {
                         Log.w(TAG, "Web dialog encountered an error.", error);
                     } else {
                         Log.i(TAG, "Web dialog complete: " + values);
                     }
                 }
             });
     builder.build().show();

只需要使用builder的show之後,就會出現讓我們選取分享給朋友的頁面,在選取完成並按下傳送之後就會將訊息傳送給朋友。

比較奇怪的地方是setTitle和setMessage都沒有反應不知道用途在哪


在訊息傳送出去之後我原本以為會顯示在通知訊息的欄位,但是訊息通知卻顯示在遊戲邀請的位置

這時候就必須去facebook developer app 開發頁面做設定

在選擇平台的地方選擇 app on Facebook

設定Canvas URL and Source Canvas URL
在設定完成之後,再重新送一次通知,這次通知不再是出現在遊戲邀請欄位,
而是出現在下圖最右邊的通知中


這樣只要每次傳送app使用邀請,朋友就可以直接在通知看到,而不再需要刻意前往遊戲邀請(應該不會有幾個人會刻意過去那邊找),對於user來說,這樣也會有較好的user experience。

Android 開發 (三十八) facebook friendPicker

FriendPickerFragment 是 facebook sdk內含的fragment,初始化這個fragment必須擁有幾個參數
        intent.putExtra(FriendPickerFragment.USER_ID_BUNDLE_KEY, userId);
        intent.putExtra(FriendPickerFragment.MULTI_SELECT_BUNDLE_KEY, multiSelect);
        intent.putExtra(FriendPickerFragment.SHOW_TITLE_BAR_BUNDLE_KEY, showTitleBar);

userId 為顯示哪個user的朋友,預設為登入本人  (null)
multiSelect 是否可以多選
SHOW_TITLE_BAR_BUNDLE_KEY 文件並沒有解釋,如果設為false 就不會出現 Choose Friends的title 以及 done的按鈕,如下圖。



在進入Picker頁面時必須將  friendPickerFragment.loadData(false); 設為false,這樣才會開始load friend list,使用getSelection讓我們可以取得選取的friendList,在我們選取完成後,接著必須專注在如何將資料傳回呼叫端。

使用startActivityForResult讓我們在選取完成並按下done之後可以將資料回傳給呼叫者,
但是在這邊會遇到一個問題,List<GraphUser> 並不能利用 intent 傳送資料
所以在 facebook的sample code裡面,利用了application來傳遞資料,
我覺得這不是一個很好的解決方案,所以花了點時間去思考較好的方法,
讓我們看看下面的sample Code


             Intent intent = new Intent();
             ArrayList<String> vals = new ArrayList<String>();
             for(GraphUser user: friendPickerFragment.getSelection()){
              vals.add(user.getInnerJSONObject().toString());
             }
 
             
             intent.putStringArrayListExtra("list",vals);
                setResult(RESULT_OK, intent);
                finish();

利用getInnerJSONObject().toString() 將資料轉成String 並且利用 putStringArrayListExtra將資料放進intent以便於傳送至呼叫者,在呼叫端利用下面的程式碼還原

  List<String> vals= new ArrayList<String>();
      vals = data.getStringArrayListExtra("list");
      String result="";
  try {
   for (String val : vals) {
    JSONObject jsonObj;
    jsonObj = new JSONObject(val);

    GraphUser user = GraphObject.Factory.create(jsonObj,
      GraphUser.class);
    result += user.getName()+"\n";
    Log.d("Ted", user.getName());
   }
  } catch (JSONException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
將取得的String轉成JSONObject之後,再利用GraphObject.Factory 轉回GraphUser,
這樣我們就可以取得原本的資料了(雖然要經過一小段的轉換)。

最近又有機會接觸 facebook sdk ,我想之後應該還會有幾篇相關的介紹吧!!

2014年4月1日 星期二

Android 開發 (三十七) facebook login

在使用facebook sdk的時候,第一個步驟就是必須先登入
相關的前置設定可以參考Android 開發 (九) Facebook GraphApi Explorer

在相關設定設定完成之後,只需要使用UiLifecycleHelper
在這邊要特別注意,必須在每個state都使用 helper的method
範例如下


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        lifecycleHelper = new UiLifecycleHelper(this, new Session.StatusCallback() {
            @Override
            public void call(Session session, SessionState state, Exception exception) {
                onSessionStateChanged(session, state, exception);
            }
        });
        lifecycleHelper.onCreate(savedInstanceState);

    }
    
    @Override
    protected void onResume() {
     // TODO Auto-generated method stub
     super.onResume();
      lifecycleHelper.onResume();
     
    }
    @Override
    protected void onStop() {
     // TODO Auto-generated method stub
     super.onStop();
      lifecycleHelper.onStop();
    }
    
    @Override
    protected void onDestroy() {
     // TODO Auto-generated method stub
     super.onDestroy();
      lifecycleHelper.onDestroy();
      
    }
    
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
     // TODO Auto-generated method stub
     super.onActivityResult(requestCode, resultCode, data);
      lifecycleHelper.onActivityResult(requestCode, resultCode, data);
    }

如果沒在每個state使用該使用的method將可能造成fb登入功能不正常,一直無法正常登入。
最近發現了fb登入不正常的問題,花了好久才找到這個原因......