2014年4月13日 星期日

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登入不正常的問題,花了好久才找到這個原因......