用 Google Apps Script 建立 Google Calendar event

Google Apps Script 是一套以 JavaScript 造的 API,可以讓你寫程式控制 Google Apps 內 Docs、Spreadsheet、Gmail、Drive 等等的功能。這次介紹如何用 Google Apps Script 建立 Google Calendar 的 event。我們會把部分建立 event 時所需要的資料放入 spreadsheet 內(會以 2019 年香港公眾假期作例子),然後用 Google Apps Script 讀取 spreadsheet 的內容再建立 event,效果就像 mail merge 般。

Read More

八達通新方向

最近,八達通終於做應該做的事了:商戶可以用八達通提供的商用版 app 經 NFC 向實體卡扣錢,小商戶就毋須租拍卡機就能接受八達通付款,亦都毋須使用 O! ePay 的 QR code 功能。

八達通當初就是一張單純的儲值卡。八達通開拓流動支付應該要數到在 Android 2.3 (Gingerbread) 支援 NFC 的時候見到有其他 Android 開發者推出查閱餘額 app 就學人推出一個查閱交易記錄 app(因為卡內的交易紀錄被加密,所以其他 app 只可以查閱餘額)。其後亦推出了八達通 SIM 卡,但現在應該終止推廣了。

Read More

Linkify 自動轉換成網址

最近工作需要將不完整的網址變成網址,但輸入的 string 可以是普通的文字,亦可以是一個沒有 http://https:// 的網址,亦可以一個完整的網址。但 TLD 有太多,如果自己寫 regular expression 做檢查的話那句 regular expression 就會好長,而且要定時補上日後新推出的 TLD。

之後查過原來 Android 有 Linkify/LinkifyCompat 這個 class:

Linkify take a piece of text and a regular expression and turns all of the regex matches in the text into clickable links. This is particularly useful for matching things like email addresses, web URLs, etc. and making them actionable. Alone with the pattern that is to be matched, a URL scheme prefix is also required. Any pattern match that does not begin with the supplied scheme will have the scheme prepended to the matched text when the clickable URL is created. For instance, if you are matching web URLs you would supply the scheme http://. If the pattern matches example.com, which does not have a URL scheme prefix, the supplied scheme will be prepended to create http://example.com when the clickable URL link is created.

看起來應該合用,不過一般用法都是用來將 TextView 的合適文字變成可點擊的超連結。下面是我的做法:

1
2
3
4
5
6
7
val query = "example.com"
val spannable = SpannableString(query)
LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS)
val linkifyUrl = spannable.getSpans(0, spannable.length, URLSpan::class.java)
.filter { urlSpan -> spannable.getSpanStart(urlSpan) == 0 && spannable.getSpanEnd(urlSpan) == spannable.length }
.map { urlSpan -> urlSpan.url }
.firstOrNull()

首先將疑似網址的 string 變成 SpannableString。之後用 LinkifyCompat#addLinks 來檢查整個 string 有沒有網址。如果有的話,SpannableString 中的網址部分會有 span 包住。

這次的要求是整個 query string 最多只有一個網址,如果沒有網址或出現多於一個網址的話,就當作沒有網址處理。所以要加上 filter 來篩走出現多於一個網址的情況。

最後 linkifyUrl 會是 http://example.com;如果 query 沒有網址的話,那 linkifyUrl 會是 null

至於 LinkifyCompat 會不會定期更新 TLD 清單,我不太清楚,但當初出現 LinkifyCompat 就是用來補回新出的 TLD

Kotlin Parcelize

Kotlin Android extensions 入面有一個實驗功能:Parcelize。它是一個 annotation,只需要在 data class 加上 @Parcelize annotation 和 implement Parcelable interface 就能夠在 compile 時自動生成所需的 boilerplate。

Product.kt
1
2
@Parcelize
data class Product(val name: String, val price: Double) : Parcelable

留意要在 build.gradle 加上:

build.gradle
1
2
3
androidExtensions {
experimental = true
}

這個基本用法在不少網站都有介紹過,的確可以節省不少時間 copy and paste code,或者可以不需要再用 PaperParcel 之類的 library。不過如果要自訂個別 property 的 adapter 的話(例如那個 property 的 data type 不是自己的 class 又沒有 implement Parcelable),就可以用 @WriteWith 來註明 adapter:

Product.kt
1
2
3
4
5
6
@Parcelize
data class Product(
val name: String,
val price: Double,
val expiryDate: @WriteWith<ExpiryDateParceler> ExpiryDate
) : Parcelable

假設我們有個 ExpiryDate 的 field,在 type 前面加入 @WriteWith annotation。ExpiryDateParceler 就是我們特別為 ExpiryDate 寫的 adapter object class。

ExpiryDateParceler.kt
1
2
3
4
5
6
7
object ExpiryDateParceler : Parceler<ExpiryDate> {
override fun create(parcel: Parcel): ExpiryDate = ExpiryDate.createByFullDate(parcel.readString())
override fun ExpiryDate.write(parcel: Parcel, flags: Int) {
parcel.writeString(this.fullDate)
}
}

Parceler 有兩個 method 要實作:一個是從 ExpiryDate serialize 變成 Parcel;另一個是由 Parcel deserialize 變成 ExpiryDate。這個例子用了 string 來 serialize,你可以用其他 type 來 serialize,最重要是之後可以被還原。留意必須使用 object class。

如果 ExpiryDate 是 nullable 的話,在 ExpiryDateParcelerExpiryDate 改成 ExpiryDate? 即可。

React Native Android Multi-window 多視窗支援

Android 7.0 (N) 新增一次顯示多個 app 功能 (Multi-window)。即是可以兩個 app 上下或左右並排。如果想你的 React Native app 能支援這個功能的話,首先要檢查 build.gradle 的 SDK 版本(24 或以上)。

之後在 AndroidManifest.xml 應該會找到下面類似的 <activity>

1
2
3
4
5
6
7
8
9
10
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

android:configChanges 補上 smallestScreenSizescreenLayout

1
2
3
4
5
6
7
8
9
10
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

之後就可以支援這個功能了。

修改 android:configChanges 的原因是因為 Android 在進行 Multi-window 動作時(例如由一個視窗變成兩個視窗並排顯示時、視窗尺寸改變時),都會被當作 configuration change 處理。即是 activity 會執行 onDestroy 之後再執行 onCreate。但 React Native 是由它自己處理 configuration change,所以 React Native 的 project 就在 <activity> 加入 android:configChanges,令原本因旋轉畫面之類的 configuration change 都不會執行 onDestroyonCreate,重新整理 activity 界面就交由 React Native 處理。但 Multi-window 是有新的 android:configChanges 常數,所以現在就要補回,否則用 Multi-window 會把 React Native app 的狀態掉失。

Parcelable & Intent extra

Android 如果想將自己寫的 data type 的 object 傳到其他 ActivityFragment 之類的地方的話,就要用 Parcelable 來做 serialization/deserialization。Parcelable 有點像 Java 本身的 Serializable,不過 Parcelable 是 Android SDK 內專為 Android 而特設的,所以會快過 Serializable

最近寫 Android app 時無意中發現 IntentgetParcelableExtra return 出來的 object 是會重用的。如果我有個 object 用 Parcelable intent extra 在 Activity A 傳去 Activity B,而在 Activity B 用 getParcelableExtra 取回這個 object 然後再改一下 object 的 field,之後返回 Activity A 再傳相同的 object 去 Activity B,用 getParcelableExtra 取回這個 object 是會取得先前在 Activity B 改動過的 object,而不是 Activity A 那個原本的 object。

所以如果打算會改動 getParcelableExtra 傳回的 object 的話,最好都是複製一個來改,不要直接改傳回的 object。如果是 Kotlin data object 的話,可以用 copy 這個 method。

另外,Kotlin 1.1.4 的 Android Extensions plugin 新增了 @Parcelize annotation 來自動生成 Parcelable 相關的 code。但它是實驗功能,還未有正式說明文檔。如果不想用實驗功能的話,可以用 PaperParcel 之類的 library。

SemVer

剛剛為了方便做 force update app 功能的版本號碼比對就寫了一個 Semantic Versioning (SemVer) 的 Kotlin data class。這個 class 有 implement Comparable,是參照 SemVer 規範要比對 major、minor、patch 和 pre-release version,但 equals 就會再比對 build metadata(即是 Kotlin data class 的預設做法)。

Read More

Kotlin for Android

在四月開始轉用 Kotlin 來寫自己的 Android app。其實上年八月左右已經留意到 Kotlin 這個 JVM 語言能在 Android app 開發時使用,不過那時因為沒有太多時間所以只是看了少許官方教學和一些外國網誌就作罷,沒有真正拿來寫 Android app。到了最近看到愈來愈多人開始轉用 Kotlin 所以才真正開始轉用。到了現在 Kotlin 更成為 Android first-class support language。

初初轉用時都有些地方不明白,需要經常查閱文檔和 Google 例子。但其實 Kotlin 都不算太難學,syntax 上和 Java 有不同但差異不算太大,再加上一些當代語言常見的特性。所以如果本身有學過其他語言的話會很快上手。Kotlin 誕生的原因是 JetBrains 用 Java 開發 IDE 時發現到 Java 的不足而令他們決心做一個新的語言,所以骨子裏有着 Java 的影子,而 Kotlin 本身都是 JVM language(即是 Kotlin 原碼會變成 JVM bytecode 然後用 JVM 來執行)。現在 Kotlin 除了 compile 成 JVM bytecode 之外,還可以轉換成 JavaScript 和 native(即是直接在作業系統上執行,不需要 JVM/Node)。

Read More