Android Weekly Issue #220, 中文筆記. ...
Android Weekly Issue #220
August 28th, 2016
Android Weekly Issue #220
ARTICLES & TUTORIALS
Manage dependencies versions with gradle extra properties
依賴管理的小Tip: 把依賴的版本號作為變數管理.
改造之後, build.gradle文件變成這樣:
apply plugin: 'com.android.application'
android {
...
}
...
ext {
supportLibraryVersion = '23.4.0'
playServicesVersion = '9.2.1'
}
dependencies {
// support libraries
compile "com.android.support:appcompat-v7:$supportLibraryVersion"
compile "com.android.support:design:$supportLibraryVersion"
compile "com.android.support:percent:$supportLibraryVersion"
compile "com.android.support:cardview-v7:$supportLibraryVersion"
compile "com.android.support:gridlayout-v7:$supportLibraryVersion"
//play services
compile "com.google.android.gms:play-services-location:$playServicesVersion"
compile "com.google.android.gms:play-services-gcm:$playServicesVersion"
// other dependencies
...
}
定義了版本號變數, 原來hardcode時的單引號變成了雙引號, 然後用$符號取變數值.
上面這個是app module裡面使用的例子, 如果你的應用有多個module怎麼辦呢?
當然一種辦法是每個module里定義一組版本號變數, 更方便的辦法是在項目工程總目錄的build.gradle文件里定義變數.
可以在工程的build文件里寫
ext {
// sdk and tools
minSdkVersion = 14
targetSdkVersion = 23
compileSdkVersion = 23
buildToolsVersion = '23.0.2'
// dependencies versions
supportLibraryVersion = '23.4.0'
playServicesVersion = '9.2.1'
}
也可以這樣定義:
project.ext.supportLibVersion = '24.0.0'
使用的時候可以這樣取值: $rootProject.supportLibraryVersion
.
也可以省略前面的rootProject, 直接取$supportLibraryVersion
Android CI with Docker
作者講了他怎麼用Docker搭建CI.
環境:
首先, CI需要Android環境(JDK 7&8, Android SDK, Gradle, Release keychain, google-services.json, etc).
裝了這些環境之後, 需要保證他們在每一個CI實例上都是同步更新的.
用了Docker之後, 更新環境的步驟變為:
更新你的Dockerfile -> Push到版本管理系統 -> CI會build新的image, 然後push到docker registry.Build:
docker run -v ./app:/opt/app docker-ci-android:latest gradle assembleRelease
Test:
有兩種測試, 一種是單元測試, 只需要JVM; 另一種是UI或者功能測試, 需要Android.
emulator會有一些問題: why
所以你可能想要在更真實的機器上測試: STF提供了服務, 你只需要用這個stf-client.Deploy:
部署用一些gradle的task就可以完成.
fabric
gradle-play-publisher
後面還提到了一些擴展和問題.
Bottom Sheets in Android
BottomSheet是support library 23.2加入的, 是從底部滑上來的一個塊塊, 用來向用戶展現更多內容.
Support Library提供了:
BottomSheetBehavior
: 加在CoordinatorLayout
的直接child view上, 然後在java代碼里get出來, 設置state控制其狀態.
有HIDE, COLLAPSED和EXPANDED三種狀態, 分別對應隱藏, 展開到指定高度(peekHeight)和完全展開.
BottomSheetDialog
:
BottomSheetDialogFragment
.
Behaviour是給View加行為, 後面這兩種是更加模塊化的dialog, 狀態控制都一樣.
這裡推薦一下筆者自己的demo: AndroidDesignWidgetsSample
再推薦一下這篇文章裡面的Bottom Sheets部分: CodePath-Handling-Scrolls-with-CoordinatorLayout
Certificate public key pinning using Retrofit 2
SSL handshake, 交換了證書(Certificate), 這樣客戶端就可以通過證書來驗證伺服器的身份.
什麼是Certificate public key pinning呢? 也叫作SSL pinning.
把host name和public key關聯起來, 這個public key將用來和證書中的public key比較, 如果匹配了, 就證明你正在和正確的server通信.
而直接pinning證書相比pinning public key更容易一些, 但是也有不好的地方, 如果網站(比如Google)經常輪換證書(rotate its certificate), 你的應用就也得經常更新, 而這種情況一般證書裡面的public keys是保持不變的.
如何在Android中用Retrofit實現pinning呢?
首先需要網站的public key的hash, 有很多獲取方法, 參見okhttp3-CertificatePinner.
然後構建CertificatePinner類對象, 加到OkHttpClient上.
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add("api.github.com", "sha256/6wJsqVDF8K19zxfLxV5DGRneLyzso9adVdUN/exDacw=")
.build();
final OkHttpClient client = httpBuilder.certificatePinner(certificatePinner).build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(END_POINT)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();
TLSv1.2從Android16+開始支持, 但是對於20+的設備預設是disabled的, 為了強制獲取支持, 可以繼承SSLSocketFactory, 強制設置為enabled, 代碼見原文吧.
Github上有完整的代碼PublicKeyPinning
作者最後還推薦了一個測試的工具mitmproxy.
Isometric AnimatedVectorDrawable - Part 3
作者繼續講了他如何構建方塊地形圖的動態效果.
一個AnimatedVectorDrawable的xml文件實際上是用來建立一個映射關係, 關聯objectAnimators和VectorDrawable上的獨立元素. 我們可以建立一個objectAnimator, 操縱我們的一塊元素的動畫效果.
文中實現了讓方塊地形動起來的動畫效果.
The many flavors of commit()
FragmentTransaction的提交方法:
support library的FragmentTransaction
現在提供了四種不同的方法來commit一個transaction:
commit()
commitAllowingStateLoss()
commitNow()
commitNowAllowingStateLoss()
這篇文章分析了這四個方法的不同.
commit() vs commitAllowingStateLoss():
用commit()
提交有時候會遇到IllegalStateException
, 說你在onSaveInstanceState()
之後提交, 這裡有另一個文章很好地分析了這個問題:Fragment Transactions & Activity State Loss
commit()
和commitAllowingStateLoss()
在實現上唯一的不同就是當你調用commit()
的時候, FragmentManger會檢查是否已經存儲了它自己的狀態, 如果已經存了, 就拋出IllegalStateException
.
那麼如果你調用的是commitAllowingStateLoss()
, 並且是在onSaveInstanceState()
之後, 你可能會丟失掉什麼狀態呢?
答案是你可能會丟掉FragmentManager的狀態, 即save之後任何被添加或被移除的Fragments.
舉例說明:
1.在Activity里顯示一個FragmentA;
2.然後Activity被後臺, onStop()
和onSaveInstanceState()
被調用;
3.在某個事件觸發下, 你用FragmentB replace FragmentA , 使用的是 commitAllowingStateLoss()
.
這時候, 用戶再返回應用, 可能會有兩種情況發生:
1.如果系統殺死了你的activity, 你的activity將會重建, 使用了上述步驟2保存的狀態, 所以A會顯示, B不會顯示;
2.如果系統沒有殺死你的activity, 它會被提到前臺, FragmentB就會顯示出來, 到下次Activity stop的時候, 這個包含了B的狀態就會被存下來.
(上述測試可以利用開發者選項中的”Don’t Keep Activities”選項).
那麼你要選擇哪一種呢? 這就取決於你提交的是什麼, 還有你是否能接受丟失.
commit(), commitNow() 和 executePendingTransactions():
使用commit()
的時候, 一旦調用, 這個commit並不是立即執行的, 它會被髮送到主線程的任務隊列當中去, 當主線程準備好執行它的時候執行.
popBackStack()
的工作也是這樣, 發送到主線程任務隊列中去. 也即說它們都是非同步的.
但是有時候你希望你的操作是立即執行的, 之前的開發者會在commit()
調用之後加上 executePendingTransactions()
來保證立即執行, 即變非同步為同步.
support library從v24.0.0開始提供了 commitNow()
方法, 之前用executePendingTransactions()
會將所有pending在隊列中還有你新提交的transactions都執行了, 而commitNow()
將只會執行你當前要提交的transaction. 所以commitNow()
避免你會不小心執行了那些你可能並不想執行的transactions.
但是你不能對要加在back stack中的transaction使用commitNow()
, 即addToBackStack()
和commitNow()
不能同時使用.
為什麼呢?
想想一下, 如果你有一個提交使用了commit()
, 緊接著又有另一個提交使用了commitNow()
, 兩個都想加入back stack, 那back stack會變成什麼樣呢? 到底是哪個transaction在上, 哪個在下? 答案將是一種不確定的狀態, 因為系統並沒有提供任何保證來確保順序, 所以系統決定乾脆不支持這個操作.
前面提過popBackStack()
是非同步的, 所以它同樣也有一個同步的兄弟popBackStackImmediate()
.
所以實際應用的時候怎麼選擇呢?
- 如果你需要同步的操作, 並且你不需要加到back stack里, 使用
commitNow()
.
support library在FragmentPagerAdapter里就使用了commitNow()來保證在更新結束的時候, 正確的頁面被加上或移除. - 如果你操作很多transactions, 並且不需要同步, 或者你需要把transactions加在back stack里, 那就使用
commit()
. - 如果你希望在某一個指定的點, 確保所有的transactions都被執行, 那麼使用
executePendingTransactions()
.
Break circular dependency with RxJava 用RxJava打破迴圈依賴.
當你把代碼分成各個部分, 比如用MVP, 這些各個部分之間可能會有相互依賴, 比如View需要Presenter, Presenter也需要View.
作者也沒有說雙向關聯有什麼缺點, 但是他說RxJava可以把這種雙向的依賴改成單向的.
作者的辦法是使用RxBinding把button的click事件變成一個Observable, 然後Presenter監聽click這個Observable, 後面接一個flatMap, 裡面髮網絡請求, 得到結果之後再調用view的方法.
這麼一改以後View中就不需要再持有Presenter的引用了.
舉這個例子, 最後是想說, 如果你想從A中調用B的非同步方法, 你不用總是在A中保存一個B的引用, 你可以把A中的事件作為一個Observable. 這樣只需要B保存了A的引用就可以了.
Asynchronous layout inflation 非同步解析layout
最近的support library revision 24中, Google的開發者在v4包中加入了一個新的輔助類AsyncLayoutInflater, 來實現佈局的非同步解析.
我們現在常用的佈局解析inflate方法都是同步的, 那什麼時候需要非同步地做這件事情呢?
比如你想延遲載入佈局中的一塊, 或者你想把佈局解析作為用戶某個交互的一個響應. 這樣就可以用這個非同步佈局解析類, 保證了主線程在inflation進行的時候仍然可響應.
怎麼使用呢?
首先, 在主線程創建對象AsyncLayoutInflater(this)
,
用它inflate佈局的時候第三個參數是一個OnInflateFinishedListener
回調.
以前同步方法的第三個參數是一個boolean, 說佈局是否需要attach到parent上, 現在沒有這個boolean參數了.
當然, 使用非同步解析也有缺點:
- 父類方法
generateLayoutParams()
必須是線程安全的. - 被創建的所有View不能創建Handler,或者調用
Looper.myLooper()
方法. - 不支持設置
LayoutInflater.Factory
和LayoutInflater.Factory2
- 不支持佈局里有Fragment.
如果我們要非同步inflate的佈局不能支持非同步, inflate的過程將會自動轉化為在UI線程的解析.
作者文中附有Kotlin的例子.
Introduction to Automated Android Testing - Part 5
系列文章的第五篇, 之前第四篇的時候寫了Presenter, 定義了V和P的介面, 本篇接著寫View介面的實現.
這裡Presenter和View關聯作者寫了兩個attachView()和detachView()方法, 前者在Presenter構造之後調用, 後者在Activity的onDestroy()里調用. 這裡同時會unregister RxJava的subscriptions, 避免了記憶體泄露的發生.
作者在佈局時用了ConstraintLayout
, 關於這個layout的使用她有另一個blog
另外作者還加了Toolbar上的SearchView, 到此, 作者的這個app就基本完成了.
作者的代碼里還有一個Injection類, 用來提供retrofit的service, 即代碼中UserRepo的獲取, 在Presenter構造時傳入.
作者的代碼: GithubUsersSearchApp
預告下一篇將會加入UI測試.
DiffUtil is a must!
support library 24.2.0推出了一個新的輔助類DiffUtil
, 它是用來解決什麼問題的呢?
如果你的RecyclerView.Adapter第一次接收到了新的數據, 這很簡單, 只需要將它們顯示出來, 但如果已經有了數據, 新的數據又來了, 這時候怎麼做才是最好的呢?
DiffUtil來了, 它就是專門為瞭解決RecyclerView的Adapter更新而設計的, 他可以計算出前後兩個list的不同, 然後返回一組更新操作, 把第一個list變為第二個list.
DiffUtil
需要知道你的兩個list的基本信息: 長度, 基本item的比較.
DiffUtil.Callback
是用來向DiffUtil
提供這些基本信息的, 它是一個抽象類, 你需要繼承它, 然後覆寫裡面的幾個方法. 它的構造傳入了兩個待比較的list, 覆寫的方法主要是get它們的size, 比較它們的內容.
Callback里還有一個getChangePayload()
方法, 它不是抽象的, 這個方法在areItemsTheSame()
返回true
, 但是areContentsTheSame()
返回false
的時候被調用.
這意味著我們的item還是之前的那個item,但是可能裡面的欄位變化了.
這個方法的返回值即為兩個對應item的diff, 基本來說, 這個方法返回的是為什麼我們認為list變化了.
文中的代碼例子返回了一個Bundle, 把compare不相等的欄位都放進去了, 用的是new item的值.
一旦我們寫好了這個Callback類, 剩下的事情就很簡單了, 我們只需要在新數據到來的時候計算一下diff, 然後更新.
@ Override
public void onNewProducts(List<Product> newProducts) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new ProductListDiffCallback(mProducts, newProducts));
diffResult.dispatchUpdatesTo(mProductAdapter);
}
當然上面getChangePayload()
返回的對象還得我們自己利用起來, 它會被DiffResult
分發到Adapter.
用的是notifyItemRangeChange(position, count, payload)
方法, 傳到了Adapter的onBindViewHolder()
方法, 我們判斷payload不為空時, 從裡面拿出diff做更新.
文檔里說DiffUtil
對很大的數據集可能比較費時, 所以建議把計算放在後臺線程.
作者還給出了一個RxJava的例子, 各種flatMap.
DESIGN
Diverse Device Hands
Facebook的design資源, 很多拿著手機的手的照片.
LIBRARIES & CODE
unipiazza-android-twostepslogin
一個實現兩步登錄的庫, 比如Google web登錄, Material Design.
要用它的佈局, 然後設置一些屬性, 還有UI交互事件的Listener.
Om Recorder
一個簡單的Pcm / Wav 錄音機, API簡單, 可以錄製Pcm和Wav音頻, 可以配置輸出, 有暫停功能.
tiger
又一個依賴註入庫, 但是README里說這不算一個Google的官方產品, 官方的是Dagger和Guice.
這個tiger好像自稱是目前最快的java依賴註入.
NEWS
Taking the final wrapper off of Android 7.0 Nougat
Android 7.0已經問世了, 從Nexus開始, 同時API 24的source code已經push到AOSP了.