天天看點

Android Weekly Notes Issue #220

Android Weekly Issue #220, 中文筆記.

Android Weekly Issue #220

August 28th, 2016

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.

  1. 環境:

    首先, CI需要Android環境(JDK 7&8, Android SDK, Gradle, Release keychain, google-services.json, etc).

    裝了這些環境之後, 需要保證他們在每一個CI執行個體上都是同步更新的.

    用了Docker之後, 更新環境的步驟變為:

    更新你的Dockerfile -> Push到版本管理系統 -> CI會build新的image, 然後push到docker registry.

  2. Build:

    docker run -v ./app:/opt/app docker-ci-android:latest gradle assembleRelease

  3. Test:

    有兩種測試, 一種是單元測試, 隻需要JVM; 另一種是UI或者功能測試, 需要Android.

    emulator會有一些問題: why

    是以你可能想要在更真實的機器上測試: STF提供了服務, 你隻需要用這個stf-client.

  4. 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()

是以實際應用的時候怎麼選擇呢?

  1. 如果你需要同步的操作, 并且你不需要加到back stack裡, 使用

    commitNow()

    support library在FragmentPagerAdapter裡就使用了commitNow()來保證在更新結束的時候, 正确的頁面被加上或移除.
  2. 如果你操作很多transactions, 并且不需要同步, 或者你需要把transactions加在back stack裡, 那就使用

    commit()

  3. 如果你希望在某一個指定的點, 確定所有的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了.

作者: 聖騎士Wind

出處: 部落格園: 聖騎士Wind

Github: https://github.com/mengdd

微信公衆号: 聖騎士Wind

Android Weekly Notes Issue #220