.settext -> .setColor 可以用tab不要用enter
Log.d(TAG, "send() called with: " + "p1 = [" + p1 + "], p2 = [" + p2 + "]");
}
Find Commands : Shift + CMD + A
retrofit 是個非常方便開發的library,利用它我們可以快速開發,而且快速測試,但是該怎麼做? 今天就要來稍微說明一下,2015 Android Dev Summit提到的做法.
在說明如何測試之前,必須先說明一下gradle flavor,如果不知道gradle flavor的可以參考一下之前的文章
Gradle Flavor ,flavor 的好處是,假設今天我們有兩種flavor,
normalMode & mockMode,當我需要建置正常的版本時,我只需要使用normalMode flavor就好,當我需要建置debug mode時,我只需要使用mockMode就好了.
不過這樣有什麼好處?
讓我們繼續看下去….
同樣一個getUsers()的method
在normalMode底下回傳的資料是真正api 打到server的資料
而在mockMode時則是回傳我們自己偽造的資料
以這種idea去設計的話,我們可以想像出一種架構
interface IApiService{
Users getUser();
}
然後在normalMode時
NormalService implements IApiService{
public User getUser(){
//do api call and get User Model
//....
return user;
}
}
然後在mockMode時則是
MockService implements IApiService{
public User getUser(){
//mock test data
User user = new User();
user.name = "ted";
return user;
}
}
所以依照上面的範例,我們只需要分別在normal and mock 的folder裡將這兩個檔案放入就完成了,之後只要利用flavor的切換就可以切換成正式資料或測試資料了.
那…..講了這些又跟retrofit有什麼關係?
利用retrofit 我們可以很輕易地做到剛剛這些事情
舉個例子
SimpleService.GitHub github = Injection.getInjection();
// Create a call instance for looking up Retrofit contributors.
Observable<List<SimpleService.Contributor>> call = github.contributors("square", "retrofit");
// Fetch and print a list of the contributors to the library.
Log.d("Ted","fire");
call.observeOn(Schedulers.io())
.subscribeOn(Schedulers.newThread())
.subscribe(new Action1<List<SimpleService.Contributor>>() {
@Override
public void call(List<SimpleService.Contributor> contributors) {
for (SimpleService.Contributor contributor : contributors) {
Log.d("Ted",contributor.login + " (" + contributor.contributions + ")");
}
}
});
稍微講解一下上面的code
為了達到 更換資料的
所以我們必須想辦法讓 Injection.getInjection()丟出不同的service
是不是跟第一個sample的概念很像?
所以我們只需要在normalMode讓api真的去打server
public class Injection {
public static SimpleService.GitHub getInjection(){
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(SimpleService.API_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
return retrofit.create(SimpleService.GitHub.class);
}
}
然後在 mockMode 的時候製造假的資料
public static SimpleService.GitHub getInjection() {
// Create the Behavior object which manages the fake behavior and the background executor.
NetworkBehavior behavior = NetworkBehavior.create();
// Create the mock implementation and use MockRetrofit to apply the behavior to it.
NetworkBehavior.Adapter<?> adapter = RxJavaBehaviorAdapter.create();
MockRetrofit mockRetrofit = new MockRetrofit(behavior, adapter);
MockGitHub mockGitHub = new MockGitHub();
SimpleService.GitHub gitHub = mockRetrofit.create(SimpleService.GitHub.class, mockGitHub);
return gitHub;
}
這樣就行了,之後只需要在開發的時候切換到mockMode就可以使用假的資料去做開發了,是不是很方便啊!!
順便提一下retrofit 有提供mock的lib 讓我們可以方便的mock假的rx資料以及call<>資料
compile ‘com.squareup.retrofit:retrofit-mock:2.0.0-beta2’
compile ‘com.squareup.retrofit:adapter-rxjava-mock:2.0.0-beta2’
而且還可以利用NetworkBehavior的setDelay來延遲api回傳的時間,真的非常的方便.
最後
還是要附一下開發的連結retrofit_with_gradle_flavor
大家快點去嘗試看看吧!!
由於最近有點空閒,所以花了點時間看了一下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上做相對應的顯示.
可想而知,作法就會是
從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
由於Trunk Based Development 會使多個功能同時在同一個branch上開發,這也就衍生了另一個問題,假設某些功能是v1.0要進,某些功能是v1.1才要進,那要怎麼處理?
根據martin fowler的說法,我們必須實作feature toogle,講白了點就是類似開關的功能,根據設定檔開啟或關閉該功能,舉個例子來說,某個版本要進v1.1,但是我們的功能在v0.1 的時候就已經開始開發,所以我們必須實作一個開關,避免v1.0的版本會出現這個不應該出現的功能,接著我們可以在v1.1的時候將開關打開,然後在v1.2的時候將開關這個功能拔除並且移除舊的功能.針對介面來撰寫程式,而不要針對實作
所以在實作這個功能的時候,首先必須先將共同的邏輯抽離抽成interface or abstract,然後舊版則實作這個interface or abstract,新版也實作這個interface or abstract,然後最重要的是使用者只針對interface or abstract 做動作,然後我們就可以去做switch的動作其實這篇文章是想要稍微推薦一下markdown
markdown其實是個很輕量的語法,由於用google blog的介面寫blog真的很麻煩
這些使用markdown語法 + stackedit 之後所有的問題都迎刃而解啊!!
我想應該會使用這個editor寫一陣子吧!!
順便熟悉一下新editor的用法
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 } }
接著必須在build.gradle裡加上multidexEnableddependencies { compile 'com.android.support:multidex:1.0.0' }
<android.support.design.widget.NavigationView android:id="@+id/navigation" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" app:headerLayout="@layout/headerlayout" app:menu="@menu/menu_main" />
classpath 'com.android.databinding:dataBinder:1.0-rc0'
apply plugin: 'com.android.databinding'
<?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="user" type="com.designsupportlibrary.normaldatabinding.NormalUser"/> </data> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <EditText android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName}"/> <EditText android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.lastName}"/> <Button android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout></layout>
public class NormalUser { private String firstName; private String lastName; public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public NormalUser(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } }
tabLayout.setTabTextColors(Color.BLUE,Color.parseColor("#FF2750"));想要更換tab的顏色時,卻無法正確更換,
xmlns:android.support.design="http://schemas.android.com/apk/res-auto"才能正確使用,fabSize有分normal and mini如上圖,就是正常size和mini size
compile 'com.android.support:design:22.2.0'
<android.support.design.widget.TextInputLayout android:id="@+id/inputlayout" android:hint="name" android:layout_width="wrap_content" android:layout_height="wrap_content" > <EditText android:hint="name" android:layout_width="wrap_content" android:layout_height="wrap_content" /></android.support.design.widget.TextInputLayout>
TextInputLayout inputLayout = (TextInputLayout)findViewById(R.id.inputlayout);inputLayout.setHint("name");
ComponentName serviceName = new ComponentName(this, MyJobService.class);JobInfo jobInfo = new JobInfo.Builder(JOB_ID, serviceName) .setPeriodic(10000) .setRequiresCharging(true) .build();JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);int result = scheduler.schedule(jobInfo);
public class MyJobService extends JobService { @Override public boolean onStartJob(JobParameters jobParameters) { //do something Log.d("Ted","startJob"); return true; } }
Notification notification = new Notification.Builder(MainActivity.this).setSmallIcon(R.drawable.ic_launcher).setFullScreenIntent(contentIntent, true).addAction(R.drawable.ic_launcher,"add",contentIntent).build();
<head>
<meta property="al:android:url" content="com.mypackage://story/1234">
<meta property="al:android:package" content="com.mypackage">
<meta property="al:android:app_name" content="myappName">
<meta property="og:title" content="title" />
<meta property="og:type" content="website" />
</head>
其中url 為 app要處理的資料,利用該資料就可以知道該user是在哪個頁面,我們就可以將app導向特定的頁面<activity android:name=".MainActivity"
android:label="@string/app_name" >
...
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="com.mypackage" />
</intent-filter>
</activity>
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FacebookSdk.sdkInitialize(this);
...
Uri targetUrl =
AppLinks.getTargetUrlFromInboundIntent(this, getIntent());
if (targetUrl != null) {
Log.i("Activity", "App Link Target URL: " + targetUrl.toString());
}
}
targetUrl就是特定的網址,我們就可以利用該url判斷應該導到app的哪個頁面curl https://graph.facebook.com/app/app_link_hosts \
-F access_token="APP_ACCESS_TOKEN" \
-F name="Android App Link Object Example" \
-F android=' [
{
"url" : "sharesample://story/1234",
"package" : "com.facebook.samples.sharesample",
"app_name" : "ShareSample",
},
]' \
-F web=' {
"should_fallback" : false,
}'
接著你會得到一組id{"id":"643402985734299"}
接著在利用下方網址 將上面的id 帶入curl -G https://graph.facebook.com/643402985734299 \-d access_token="APP_ACCESS_TOKEN" \
-d fields=canonical_url \
-d pretty=true
接著會得到一組網址{
"canonical_url": "https://fb.me/643402985734299",
"id": "643402985734299"
}
這個網址就是我們創造出來的網址,我們可以將這個網址貼到facebook上,只要有人點擊該link 有裝app 就會前往我們的app,沒有app的就會被導向googleplay