# Android Note
## Android Life Cycle
https://android.i-visionblog.com/android-fragments-life-cycle-612e85c047dd
If you want to get data from network, you must call data in **Resume** stage. In other stage, you may get null pointer exception with view component when user smooth fast.

When your app is in background, you need backup data.
Notice that onDestroy is not called everytime. So backup data you must in onSaveInstanceState.
Here is life relationship.

Configure.user is a god object. But app is killed by android, it's data will be gone.
I backup it in onSaveInstanceState, when app recreate it restore in onRestoreInstanceState.
``` kotlin=
override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle?) {
outState.putSerializable(BACKUP_USER, Configure.user)
super.onSaveInstanceState(outState, outPersistentState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
Configure.user = savedInstanceState.getSerializable(BACKUP_USER) as User
super.onRestoreInstanceState(savedInstanceState)
}
```
## Activity restart from icon in save power
//if user use medium power save than use icon open app, this flag will record root acticity's data.
android:alwaysRetainTaskState="true">
## Fragment Animation
Fragment call back method,你可以 override 這個方法,修改 fragment 轉場的動畫。
```
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation {
return super.onCreateAnimation(transit, enter, nextAnim)
}
```
## App terminate by android
test you app when it terminate by android.
Q : How to solve object data loss?

## AlertDialog setView
Not support ConstraintLayout. If you use recyclerView in AlertDialog setView by constraintLayout, it will become nothing(set match_constraint). Because dialog doesn't have size. You can set wrap_content, but recyclerView wrap_content calculation go something wrong.
So you must set this attribute after it calculation(View).
This attribute is important.
important!!
Here is ConstraintLayout deal wrap_content
```
WRAP_CONTENT : enforcing constraints (Added in 1.1)
If a dimension is set to WRAP_CONTENT, in versions before 1.1 they will be treated as a literal dimension -- meaning,
constraints will not limit the resulting dimension.
While in general this is enough (and faster), in some situations,
you might want to use WRAP_CONTENT, yet keep enforcing constraints to limit the resulting dimension.
In that case, you can add one of the corresponding attribute:
app:layout_constrainedWidth=”true|false”
app:layout_constrainedHeight=”true|false”
```
# View translationX X RawX
x座標是基於父 View 所產出的,是 View 開始畫的起點, left 則是 view 可以開始畫的範圍, x= left + translationX
,當 View 產生偏移(例如: padding)這時就會有 translationX 的產生。
RawX 則是基於螢幕左上角所產生的座標。
## keyboard
onLayoutLintener 可能會導致 view 沒有畫完 造成畫面不完全
## MVP
Activity is the overall controller which creates and connects views and presenters.
Presenter can't have Activity or Fragment.It can have view!!
Code look like this
```
MainActivity implements View
class MainPresenter{
var view:View
constructure(v:View){
this.view=v
}
}
```
What if presenter want to use sharePreference?
You must encapulate sharePreference with an interface.Because SharePreference is View layer.
Like this
```
interface SharePreferenceManager{
fun getData():String
}
class SharePreferenceMgr : SharePreferenceManager{
...
}
class MainActivity{
fun onCreate(){
var share=SharePreferenceMgr(this)
presenter.setShare(share)
}
}
```
# MVVM In Android
ViewModer life is binded with Activity or Fragment.
So if Activity or Fragment is dead, ViewModel should be dead.
What if you want to pass value between Activitys.
You can use these
1. Use Intent carry value to other Activity
2. Borcast to other Activity
3. God Object(Singleton)
# How Java GC work?
Not Reference counter!!!
It use GC tree to recycle.
https://www.jianshu.com/p/35cd012eeb8c

## Clean Architecture

## Notification Android 8
Once you submit the channel to the NotificationManager, you cannot change the importance level. However, the user can change their preferences for your app's channels at any time
也就是說在同 channel 調整這些設定需要解除安裝 APP

## Intent pass custom data in pendding intent
如果 **傳送的值** 是 **自己定義的 class** 當 **系統改變 intent 裡面的值** 時,會導致 **系統 無法將資料轉換回原始的 Type**,所以不能使用 serialize 傳遞資料 給 receiver。
## Use LiveData
Project Structure
Reposity:A Repository class handles data operations. 他是用於資料的操作,Ex:從資料庫、網路 存取資料。
LiveData:新的元件,可以使用像 Observer Pattern 的功能。
Room:Android 用於存取SQLite的框架。
ViewModel:持有資料的物件,他負責與 Reposity 拿取資料,Reposity 知道如何存取資料的行為。
## Android Unit Test
https://ithelp.ithome.com.tw/articles/10196219
## Deep Link
learn
Deep Links to App Content
https://developer.android.com/training/app-links/deep-linking
## 零散筆記
舊方法
ItemTouchHelper 可以實做 RecyclerView 的右滑 刪除
itemTouchHelper.attach(RecyclerView)
需要 query 一次,接著再 呼叫 notifyDataSetChange()
需要管生命週期
新方法(Component)
viewModel.delete(data)
不須管生命週期
Snackbar.make(fab,"Deleted xxx")
.setAction("Undo",ClickListener{
viewModel.save(data)
}).show()
paging list 's DataSource.Factory
The PagedList loads content dynamically from a source. In our case, because the database is the main source of truth for the UI, it also represents the source for the PagedList. If your app gets data directly from the network and displays it without caching, then the class that makes network requests would be your data source.
A source is defined by a DataSource class. To page in data from a source that can change—such as a source that allows inserting, deleting or updating data—you will also need to implement a DataSource.Factory that knows how to create the DataSource. Whenever the data set is updated, the DataSource is invalidated and re-created automatically through the DataSource.Factory.
problem
按返回 activity 被 destroy 然後從 多工的視窗 開啟,是原來的 task,如果從 icon 打開 會是新的 task
root Activity 沒設定 singleTask
跳轉到 B Activity,按 home 後,按 icon 打開是 B Activity。
設定 singleTask,按 home 後,按 icon 打開會是 A
# 時間計算的 tip 非常重要
如果沒有要每秒都更新 UI 的話,可以記錄 開始的時間,需要計算經過多少時間 就使用 當前時間 - 開始的時間 即可
timer 會有 cpu 導致計算不準確的問題 以及 **效能問題**
# MeasureSpec
https://medium.com/@as1234ert/measurespec-%E7%82%BA-child-view-%E6%B8%AC%E9%87%8F%E9%AB%98%E8%88%87%E5%AF%AC-f73f3fde568e

與 Super View 合作去 measure 大小的關係表
lp 代表的是 LayoutParams

# Room (Share fail)
Point 1 @Primary Key will override id default value
```
class User{
@PrimaryKey
var id:Int=0
}
```
Point 2 Don't clear what people ask.
Point 3 Forget what I code.
## Room trap
If you migrate your sqlite, you will find foreign key is an array in room.
When you use sql command ,it will not replace that array instead it add Foreign keyin array.

# Androuid View
## Layout tip
Use empty view in scroll view or other view which can scroll.
You can make coordinateLayout effect.
## EditText Focus tip
If you want to disable editText edit, you can use this.
```
var e = EditText(it)
//use both it will lead less bug
e.isFocusable //old phone have concrete keyboard
e.isFocusableInTouchMode
e.setOnEditorActionListener { v, actionId, event -> true }
```
If you just set focus false, it will lead focus find next exception.
You need to add this.
e.setOnEditorActionListener
It can lead focus to next view or not lead.
## Android View 高度
```
```
# Gson TypeToken
## TypeToken
## Using Genric Conver JSON
``` kotlin=
var type = object : TypeToken<Wrapper<String>>() {}.type
var gson = GsonBuilder().create()
var wrapperString = gson.fromJson<Wrapper<String>>
(it.errorBody()!!.string(),
type)
```
You can use this to more fixable
```kotlin=
inline fun <reified T> Gson.fromJson(json: String) =
this.fromJson<T>(json, object : TypeToken<T>() {}.type)
```
```kotlin=
Gson().fromJson(json,Wrapper<String>::class.java)
```
# onLayoutChange Listener Problem
call layoutChange twice and gridLayout not draw view when row column is same. Even if you call invaliadte or requestLayout.
# Android Splash Picture( 載入 APP 的跳轉畫面 )
https://juejin.im/post/58c5e23561ff4b005d9d21dfhttps://juejin.im/post/58c5e23561ff4b005d9d21df
# Java Thread
``` Java
public class VolatileExample {
private static boolean running = false;
private static Integer a = new Integer(10);
public static void main(String[] argu) throws Exception {
new Thread(new Runnable() {
@Override
public void run() {
while (!running) {
}
System.out.println("Started.");
while (running) {
}
System.out.println("Stopped.");
}
}).start();
Thread.sleep(1000);
System.out.println("Thread Start");
running = true;
Thread.sleep(1000);
System.out.println("Thread Stop");
running = false;
}
}
Result:
Thread Start
Thread Stop
```
Thread 居然沒有 stop!!!
請看此圖,CORE 1 負責 main thread , CORE 2 負責 thread 2,當 Thread 需要某個參數時,會從 L3 把值複製到 自己的 L1 或 L2,此時修改 running(true false) 變數只會修改到 自己的 L1 或 L2,可能幾個週期後才會把值寫入 L3,造成其他的 Thread 無法馬上讀到修改後的值,這時如果加入關鍵字 volatile,則會強制要讀取或修改值的 Thread 每項操作一定要經過 L3。
Volatile 用在 singleton 則是強制一定要一次 create 完 object 之後,一次寫入 L3。讀取也是強制一定要在 L3 讀取。

Thread

生產者消費者 Lock
生產者 通知 消費者 醒來時,他還在 synchro 的區段,此時 消費者不會從 waiting 直接進入 Runnable,而是先進入 Blocked 之後 再進入 Runnable。
# Gradle Conflict 2019/3/21
Solve
https://stackoverflow.com/questions/49754281/adding-firebase-leads-to-warning-about-mixing-versions-can-lead-to-runtime-crash
使用 cmd 查看 全部的 library,發現衝突的 library,使用 implement 去 override。
```
gradlew -q dependencies app:dependencies --configuration debugAndroidTestCompileClasspath
```
如果指令發現記憶體不足
https://blog.csdn.net/zzq900503/article/details/54709440
解决方法:
1 到目錄 C:\Users\<username>\.gradle 例如我這裡是C:\Users\20313\.gradle
2 建立 gradle.properties 内容:org.gradle.jvmargs=-Xmx512m
再次執行 command 即可
---------------------
原文:https://blog.csdn.net/zzq900503/article/details/54709440
## 讓 toolbar 沒有邊界
```xml
app:contentInsetEnd="0dp"
app:contentInsetLeft="0dp"
app:contentInsetRight="0dp"
app:contentInsetStart="0dp"
```
```xml
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_margin="0dp"
android:layout_width="0dp"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="?attr/actionBarTheme"
android:minHeight="?attr/actionBarSize"
app:contentInsetEnd="0dp"
app:contentInsetLeft="0dp"
app:contentInsetRight="0dp"
app:contentInsetStart="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<android.support.constraint.ConstraintLayout
android:layout_marginLeft="0dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</android.support.v7.widget.Toolbar>
```
# Fragment 使用 FragmentManager
重要 千萬不要使用錯的 FragmentManager 會導致無法預期的錯誤
https://juejin.im/entry/57c3db5a7db2a200680b1b7b
# TextView autoSize 條件 (自動換行)

# Bottom navigation 顯示 label
```
app:labelVisibilityMode="labeled"
```
# button 在 api 21 以上有預設陰影,取消預設陰影的方法
```
style="?android:attr/borderlessButtonStyle"
```
# 保存 Fragment 的 state 可以用 trasition 的 hide and show
# Export AAR and Imort AAR
AAR not like jar is a file
https://www.youtube.com/watch?v=qNEy9L_lf0c
https://www.youtube.com/watch?v=RddETmxmRCk
# JobService JobSchedul
https://www.jianshu.com/p/9fb882cae239
# Splash View 開啟 APP 的畫面
https://android.jlelse.eu/the-complete-android-splash-screen-guide-c7db82bce565
# 圓弧(rectangle) 的 start end progress bar
Style (Progress Drawable)
```
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="8dp"/>
<solid android:color="@color/colorAccountTextGray"/>
</shape>
</item>
<item android:id="@android:id/progress">
<scale android:scaleWidth="100%">
<shape>
<corners android:radius="8dp"/>
<solid android:color="@color/colorPrimaryDark"/>
</shape>
</scale>
</item>
</layer-list>
```
# 可以使用 AppCompatTextView 動態的更換 drawable shap 的顏色哦~
TextView 預設是不可被點擊的,要使可以被點擊,請設定 clickable
EditText 要設定輸入多行,請設定 inputType= mutiLine 啥的
```
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/sendLabel"
android:clickable="true"
android:text="傳送"
android:textSize="16sp"
android:background="@drawable/home_rectangle_bg"
android:backgroundTint="@android:color/darker_gray"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:textColor="@android:color/white"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
```
要支援 API 21 以下的手機請使用
```
app:backgroundTint="@android:color/darker_gray"
```
更換顏色
```
sendLabel.supportBackgroundTintList =
ColorStateList.valueOf(
ContextCompat.getColor(this@ChatActivity, R.color.colorPrimary)
)
```
recyclerView 排列總結
https://blog.csdn.net/DJY1992/article/details/76201794
chat recyclerView
use linearLayout
reverse 可以處理鍵盤彈出 訊息被遮蔽的問題
linearLayoutManager.stackFromEnd=true 可以讓資料由 top 開始顯示
# Android Observe Process life cycle
https://developer.android.com/reference/android/arch/lifecycle/ProcessLifecycleOwner
https://developer.android.com/reference/android/arch/lifecycle/LifecycleObserver
# ConstraintLayout tip
1. Ratio 可以使用在寬高比 app:layout_constraintDimensionRatio="w,1:4",意思是 寬可變動,寬高比是 1:4,也就是說寬是 高/4
Constraint Type 可以用來對齊 被 constraint 的元件:
最左邊或最上面稱為 chain head,chain head 可以指定
layout_constraintHorizontal_chainStyle
layout_constraintVertical_chainStyle
使用layout_constraintHorizontal_weight 可以將 ConstraintLayout 使用的像 LinearLayout
https://julianchu.net/2017/09/16-constraintlayout.html
2. 如果 View 要在斜上角的話,可以使用 constraintCircle。
layout_constraintCircle 對應的 View ( A ),以 A 為基準點。
layout_constraintCircleRadius,相對應的圓心的半徑
layout_constraintCircleAngle,對於圓心相對應的角度
```
app:layout_constraintCircle="@+id/companyClipNameText"
app:layout_constraintCircleRadius="22dp"
app:layout_constraintCircleAngle="35"
```
# 手機撥打電話
"tel:" + phone + "," + 分機:是自動撥打分機;
"tel:" + phone + ";" + 分機:是需要用戶確認後撥打分機號碼
權限
```
<uses-permission android:name="android.permission.CALL_PHONE" />
```
```
val phoneCallIntent = Intent(Intent.ACTION_DIAL, Uri.parse("tel:$phoneNumber"))
startActivity(phoneCallIntent)
```
# 手機寄送 Emil
使用 chooser 來讓使用者選擇要用哪一個 email 軟體
```
val emailIntent = Intent(Intent.ACTION_SEND)
emailIntent.putExtra(Intent.EXTRA_EMAIL, email)
emailIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.jobContractEmailSubject))
emailIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.jobContractEmailText))
emailIntent.type = "message/rfc882"
startActivity(Intent.createChooser(emailIntent, getString(R.string.jobContractEmailChooseEmailClient)))
```
# 使用 ActivityForResult 會使 Activity 的 launch 失效
# 轉換 color 可以將 xml 寫在 color , background 寫在 drawable
color 使用 android:color
drawable 使用 android:drawable
# 設置 ConstraintLayout child minHeight
```
app:layout_constraintHeight_default="spread"
app:layout_constraintHeight_min="80dp"
```
# 設置 客製化的 progress drawable 在 android 4 更改顏色
要從 Drawable 下手
```
val layerDrawable = resumeProgress.getProgressDrawable() as LayerDrawable
// get drawable/apply_to_job_progress progress drawable
var progressDrawable = layerDrawable.getDrawable(1)
progressDrawable.setColorFilter(color, android.graphics.PorterDuff.Mode.SRC_IN)
```
# Android Scroll view 注意事項

# Android 注意事項


# Android api response need to translate to view model data Class
android 可以抽 module (ex: api , view)
# Android build 64 bit apk
point :
ndk{
abiFilters 'arm64-v8a','x86_64','x86','armabi-v7a'
}
```
android {
compileSdkVersion 28
defaultConfig {
applicationId "tw.com.bank518"
minSdkVersion 19
targetSdkVersion 28
versionCode Integer.valueOf(System.env.APP_VERSION_CODE ?: 1)
versionName String.valueOf(System.env.APP_VERSION_NAME ?: "2.0.0")
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
ndk{
abiFilters 'arm64-v8a','x86_64','x86','armabi-v7a'
}
}
....
}
```
# Android set support tint background color for api 19
```
ViewCompat.setBackgroundTintList
```
# 設定 textView 部分文字 的 link 與 顏色
https://www.cnblogs.com/tianzhijiexian/p/4222393.html
設定 點擊 部分 text
https://blog.csdn.net/su20145104009/article/details/50676716
# convert json state code to enum
//convert json's string to enum
//https://stackoverflow.com/questions/8211304/using-enums-while-parsing-json-with-gson
# Gradle Note
https://avatarqing.github.io/Gradle-Plugin-User-Guide-Chinese-Verision/index.html
# Use command line to run android project
1. install gradle
2. install android SDK
3. install android tools (Build tools, platform tools, platform)
4. In android project, use "gradle wrapper" command, you will get gradlew file
## Android SDK Tools
https://developer.android.com/studio

## Find Java location. If you don't want to install java, just use Android Studio Java.

## Update Tools in Linux
1. First, update apt-get
2. Second, apt upgrade "tools name"

# Android Style and Theme
## Style
It is a map where a view attribute to values.
> Name suggestion
> Widget.AppName.Toolbar(view).BlueXXX
``` xml=
<style name="xxxx" parent="..."> <!-- Map -->
<item name="android:gravity">center</item> <!-- View Attribute -->
<item name="android:textAppearance">@style/TextAppearance</item> <!-- View Attribute -->
<item name="android:padding">@dimen/spacing_micro</item> <!-- View Attribute -->
</style>
```
## Theme
Theme is a theme attrubute to values.
> Name suggestion
> Theme.AppName.BlueXXX
``` xml=
<style name="xxxx" parent="..."> <!-- -->
<item name="colorPrimary">@color/teal_500</item>
<item name="colorSecondary">@color/pink_200</item> <!-- -->
<item name="android:windowBackground">@color/background</item> <!-- This is Theme attribute not View attribute -->
</style>
```
## Selector
No States means that it matches every state.
https://www.youtube.com/watch?v=Owkf8DhAOSo
Nameing in resource
``` xml
<resources>
<color name="color_primary">...</color>
</resources>
```
Nameing in theme
``` xml
<item name="colorPrimary">...</item>
```
## Save File
```kotlin
companion object {
const val path = "518Identify/"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val externalStorage = this.getExternalFilesDir(null)
val storage = File(externalStorage, path)
if (storage.exists().not()) {
if (storage.mkdir().not()) {
Log.d("File ", "Fail to mkdir()")
}
}
Log.d("File ", "${storage.absolutePath}")
writeToFile(storage)
Log.d("File ", "${storage.canRead()}")
var dataFile = File(storage, "tmp")
var datas: List<String> = dataFile.readLines()
datas.forEach {
Log.d("File ", "$it")
}
}
```
Write file
``` kotlin
fun writeToFile(storage: File) {
val file = File(storage, "tmp")
val fileOutputStream = FileOutputStream(file)
val outputStreamWriter = OutputStreamWriter(fileOutputStream)
outputStreamWriter.append("12569")
outputStreamWriter.flush()
outputStreamWriter.close()
}
```
# Use adb to check log
```
adb logcat -v color
```
filter keyword
```
adb logcat -v color | grep -i okhttp
```
# SSL Proxy
client -> proxy -> normal ssl
We can intercepte request and response at proxy. By creating self SSL certification.
Some web will not allow this thing, they will create their SSL certification.
# Coroutine
https://developer.android.com/topic/libraries/architecture/coroutines
### Custom scope
Like MainScope, but can specify a specific thread to run on
```
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
...
scope.cancel()
```
> NOTE: with SupervisorJob, when the sub Coroutine fail, the parent Coroutine won't fail Supervisor
### Builders
* launch: launch a coroutine within the current CoroutineScope
* withContext: use for exchanging thread for sub coroutine in a CoroutineScope
* runBlocking: blocking the current thread, and resume only if the nested coroutine is complete. Since it's block, it can also to replace functions have return value.
Scope with SupervisorJob, it will protect the parent Coroutine from failing on child Coroutine. If using supervisorScope, it will use the CouroutineContext of the enclosing CoroutineScope but replace the Job with SupervisorJob.
## switch thread
```kotlin
<CoroutineScope>.launch {
// This runs on another thread
delay(10000)
withContext(Dispatchers.Main) {
// This runs on Main Thread
}
withContext(Handler(Looper.getMainLooper()).asCoroutineDispatcher()) {
// This ALSO runs on Main Thread
}
}
```
## Suspend function
With suspend, a function can be used inside a CoroutineScope, and pause the current scope until the result of the function is returned. Here's a some pattern that makes a function as suspend:
``` kotlin
// Can become a suspend function directly
suspend fun loadSimple() = <CoroutineScope>.launch {
// do something
}
suspend fun loadSimple() = withContext(Dispatchers.IO) {
// do something
}
suspend fun loadSimple() = coroutineScope {
// do something
}
-->
// Need to call suspend function to become a suspend function
suspend fun loadSimple() = runBlocking {
// call to a suspend fun
}
suspend fun loadSimple(): Simple {
// call to a suspend fun
}
```
> NOTE: Warning like “redundant ‘suspend’ modifier” will show up if a function won't become a suspend function.
If there are UI operations inside a suspend function, it's better that we specify the Dispatcher.Main with withContext at beginning. Thus we can avoid from calling it on the worker thread unintentionally and so less error prone.
And also, since we’re going to transform functions into suspend function, **it will be better if we can specify the thread inside, not from the call site, especially for those operations that can only be run on IO/Network, etc.**
In this way, we can not only won’t need to assign which thread that it need to be run on every time, but also reduce boilerplate code for switching thread. Furthermore, it will more easier to do refactor if we want to extract the block into a function call.
## Exception handling
Kotlin Coroutine Job Hierarchy by Succeed, Fail, and Cancel
Coroutine handle exception in two different way. If using launch to trigger a Coroutine and the exception is not CancellationException. The exception will be propagated automatically like normal exception way up to parent, then we can either using try-catch or CoroutineExceptionHandler to avoid application from crash.
try-catch
```kotlin
suspend fun loadSample(id: String) = coroutineScope {
try {
repo.loadSample(id)
} catch(e: Throwable) {
// do some handling
}
}
Handler
val handler = CoroutineExceptionHandler { context, exception ->
// do some handling
}
<CoroutineScope>.launch(handler) {
// do something
}
```
async
Error handling of async will be slightly different. Since it will encapsulate the exception in the result of await, we will need to try-catch the exception while calling to await.
```kotlin
suspend fun loadSample(id: String) = coroutineScope {
val deferred = async {
throw Exception("Coroutine failed")
}
try {
deferred.await()
} catch (e: Throwable) {
// do some handling
}
}
```
> **WARNING: the bigger caveat is, the async will also fail the enclosing Coroutine. So even we already use try-catch, the exception will still be propagated to the parent**
``` kotlin
<CoroutineScope>.launch {
try {
loadSample("")
} catch(e: Throwable) { <-- we can still get the exception
// do some handling
}
}
The solution can be either doing try-catch again, or use supervisorScope.
suspend fun loadSample(id: String) = supervisorScope {
// do something
}
```
## Transform to Suspend function
Basic
Retrofit API
General speaking, since Retrofit has already support Coroutine, you can simple do all the function in the same way:
add suspend at the head of function definition
remove any keywords of RxJava from the return type
```kotlin
fun getSample(id: String): Single<Sample>
-->
suspend getSample(id: String): Sample
fun getSample(id: String): Completable
-->
suspend getSample(id: String)
Normal function
fun loadSample(id: String): Single<Sample> =
repo.getSample(id)
-->
suspend fun loadSample(id: String): Sample = coroutineScope {
repo.getSample(id)
}
// or
suspend fun sample(): Sample = runBlocking {
repo.getSample(id)
}
```
### Case study
#### Zip
In theory, zip in RxJava will implicitly make the upstream to be run synchronously if the upstreams don't use subscribeOn to assign a thread.
In coroutine, there's an easy way to make them to be run asynchronously:
``` kotlin
Observable.zip(loadSample("A"), loadSample(B))
-->
// async + async + await + await -> 2 suspends in async will run asynchronously
val taskA = async { loadSample("A") }
val taskA = async { loadSample("B") }
Observable.zip(taskA.await(), taskB.awart())
// async + await + async + await -> 2 suspends in async will run synchronously.
val taskA = async { loadSample("A") }.await()
val taskA = async { loadSample("B") }.await()
Observable.zip(taskA, taskB)
// the IDE will warning that async-await will be redundent. Because we will run
// susepend function in a Scope, where the suspend will act like an normal
// blocking function
```
Flatmap
```kotlin
loadSample("A").flatMap {
loadSample("B")
}
-->
loadSample("A").flatMap {
// New observable in `flatMap` will be trigger immediately, so we can use
// runBlocking to fetch data first
val sampleB = runBlocking {
loadSample("B")
}
Single.just(sampleB)
}
doOnError / Subscribe
```
try-catch
```kotlin
try {
// do something
} catch (e: Throwable) {
// error handling
throw e // only for `doOnError`, throw again to let the caller to catch
}
doOnFinal
try {
// do something
} catch (e: Throwable) {
// error handling
} finally {
// do something
}
```
Maybe
```kotlin
Maybe.fromCallable<> {
loadSample()
}
-->
try {
loadSample()?.run { // operation for success must wait for the result
// do something
} ?: run {
// do something
}
} catch (t: Throwable) {
// do something
}
```
Combination
partially get a Single/Observable/Completable/Flowable/Maybe
```kotlin
fun getTrendingRx(): Single<Sample> {
return loadSampple
}
-->
fun getTrending(): Single<GiphyResponse> = rxSingle {
// do something
}
get result without local field
fun sample(): String =
loadSample()
-->
fun suspend sample() = runBlocking {
try {
loadSample()
} catch {
// return fallback value or throw exception without returning
}
}
```
subscribe in function directly
``` kotlin
fun loadSample() {
disposables += loadSample(args)
.subscribe({
// do something
}, {
// error handling
})
}
-->
// non-blocking thread
fun loadSample() = <some scope>.launch {
try {
loadSample(args).apply { // scope function will be useful for returning value
// do something
}
} catch (e: Throwable) {
// error handling
}
}
```
## Coroutine callback
suspendCoroutine
Kotlin ``` with ``` is a good thing.
## What's more
### Stream
WARNING: Flow is not current considering using
Flow API: https://ahsensaeed.com/introduction-new-kotlin-coroutine-flow-api/
Worker
### For a Worker, use CoroutineWorker:
https://developer.android.com/topic/libraries/architecture/workmanager/advanced/coroutineworker
Understand Kotlin Coroutines on Android (Google I/O'19)
### Reference:
Roman Elizarov
Blocking threads, suspending coroutines
Elaborate Explaie the differences between Coroutine/Thread
Deadlocks in non-hierarchical CSP
Official
Coroutine guide
Coroutine guide - UI
Kotlin Conf
Deep Dive into Coroutines on JVM
Introduction to Coroutines
Google talk
Understand Kotlin Coroutines on Android (Google I/O'19) - Workers, UnitTest
LiveData with Coroutines and Flow (Android Dev Summit '19)
Android Suspenders (Android Dev Summit '18)
Coroutines on Android (part I): Getting the background
Others
How coroutines switch back to the main thread
Differentiating Thread and Coroutine
Kotlin Coroutine Job Hierarchy by Succeed, Fail, and Cancel
Coroutines patterns & anti-patterns
https://proandroiddev.com/kotlin-coroutine-job-hierarchy-finish-cancel-and-fail-2d3d42a768a9
## Kotlin Dagger
use ``` @set:Inject ```
## Fragment manager hole
onCreate -> FragmentManager 會 retore 之前的狀態
所以這個時候 FragmentManager 會重複 create Fragment 在 FragmentManager 裡面 造成畫面重疊
Fragment default state is showed
## Video call lib
AgoraManager
## Layout inspector
## Virtual Function
讓語言擁有 override 的特性,function 跟著物件走,而不是跟著型態
# Args
Replace bundle at activity
```kotlin
package com.grindrapp.android.args
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import com.grindrapp.android.analytics.GrindrCrashlytics
import com.grindrapp.android.extensions.getMap
import com.grindrapp.android.extensions.getNonNullByteArray
import com.grindrapp.android.extensions.getNonNullParcelable
import com.grindrapp.android.ui.home.HomeActivity
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.Serializable
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.full.createInstance
/**
* Because if you use a bundle to pass data between activity / fragment, there will be a problem
* of unclear content. This will cause the developer to not know what data need to be passed in to
* start this activity, nor do they know where these data are used.
* So we decided to use args (data class) instead of bundle as the transport protocol.
*
* Here are some examples of usage:
* data class YourArgs(
* val yourValue: String
* ) : BundleArgs
*
* 1. I need pass YourArgs form activity A to B
*
* Activity A:
* val intent = Intent(context, A::class.java)
* intent.putArgs(YourArgs("your value"))
* context.startActivity(intent)
*
* Activity B:
* val args by ArgsCreator<YourArgs>()
* override fun onCreate(savedInstanceState: Bundle?) {
* print(args.yourValue)
* }
*
* 2. I need reset args when receives onNewIntent
*
* Activity B:
* val argsCreator = ArgsCreator<YourArgs>()
* val args by argsCreator
* override fun onNewIntent(intent: Intent) {
* super.onNewIntent(intent)
* setIntent(intent)
* args = argsCreator.createArgs(intent)
* }
*
* 3. My args will change content during the execution of activityB, and I want to restore the changed content when activity restore.
*
* Activity B:
* va; argsCreator = ArgsCreator<YourArgs>()
* val args by argsCreator
* override fun onCreate(savedInstanceState: Bundle?) {
* super.onCreate(savedInstanceState)
* if (null != savedInstanceState) {
* args = argsCreator.createArgs(savedInstanceState)
* }
* }
* override fun onSaveInstanceState(outState: Bundle) {
* super.onSaveInstanceState(outState)
* outState.putArgs(args)
* }
*/
interface BundleArgs : Serializable {
fun toBundle(): Bundle {
val argsKey = getClassName(this::class)
return when (this) {
is Parcelable -> bundleOf(argsKey to this)
else -> bundleOf(argsKey to toByteArray())
}.apply {
classLoader = this@BundleArgs::class.java.classLoader
}.also {
GrindrCrashlytics.log("[BundleArgs] Create $it from $this")
}
}
private fun Any.toByteArray(): ByteArray {
val byteArrayOutputStream = ByteArrayOutputStream()
ObjectOutputStream(byteArrayOutputStream)
.use {
it.writeObject(this)
it.flush()
}
return byteArrayOutputStream.toByteArray()
}
}
class ArgsCreator<ARGS : BundleArgs>(
private val kClass: KClass<ARGS>,
private val defaultArgs: (() -> ARGS)? = null
) {
companion object {
inline operator fun <reified ARGS : BundleArgs> invoke(noinline defaultArgs: (() -> ARGS)? = null): ArgsCreator<ARGS> {
return ArgsCreator(ARGS::class, defaultArgs)
}
}
private var value: ARGS? = null
operator fun getValue(activity: Activity, property: KProperty<*>): ARGS {
return try {
value ?: createArgs(activity).apply { value = this }
} catch (e: Exception) {
GrindrCrashlytics.logException(e)
activity.toHomeActivity()
createInvalidArgs(e)
}
}
operator fun getValue(fragment: Fragment, property: KProperty<*>): ARGS {
return try {
return value ?: createArgs(fragment).apply { value = this }
} catch (e: Exception) {
GrindrCrashlytics.logException(e)
fragment.activity?.toHomeActivity()
createInvalidArgs(e)
}
}
operator fun setValue(activity: Activity, property: KProperty<*>, args: ARGS?) {
value = args
}
operator fun setValue(fragment: Fragment, property: KProperty<*>, args: ARGS?) {
value = args
}
fun createArgs(intent: Intent): ARGS {
return fromBundle(intent.extras)
}
fun createArgs(bundle: Bundle): ARGS {
return fromBundle(bundle)
}
private fun createArgs(activity: Activity): ARGS {
return fromBundle(activity.intent.extras)
}
private fun createArgs(fragment: Fragment): ARGS {
return fromBundle(fragment.arguments)
}
@Suppress("UNCHECKED_CAST")
private fun fromBundle(bundle: Bundle?): ARGS {
return try {
if (null == bundle) {
throw IllegalArgumentException("bundle should not be empty")
}
val argsKey = getClassName(kClass)
val bundleMap = bundle.getMap()
GrindrCrashlytics.log("[BundleArgs] Try to create $argsKey from bundle$bundleMap")
if (Parcelable::class.java.isAssignableFrom(kClass.java)) {
bundle.getNonNullParcelable<Parcelable>(argsKey) as ARGS
} else {
bundle.getNonNullByteArray(argsKey).toObject()
}.also {
GrindrCrashlytics.log("[BundleArgs] Create $it")
}
} catch (e: Exception) {
defaultArgs?.invoke() ?: throw e
}
}
@Suppress("UNCHECKED_CAST")
private fun <T> ByteArray.toObject(): T {
val byteArrayInputStream = ByteArrayInputStream(this)
return ObjectInputStream(byteArrayInputStream)
.use {
it.readObject() as T
}
}
private fun Activity.toHomeActivity() {
val intent = HomeActivity.getIntentClearTop(this)
startActivity(intent)
}
private fun createInvalidArgs(sourceError: Exception): ARGS {
return try {
kClass.createInstance()
} catch (e: IllegalArgumentException) {
throw sourceError
}
}
}
private fun getClassName(kClass: KClass<*>): String = kClass.java.name
fun Intent.putArgs(args: BundleArgs) {
putExtras(args.toBundle())
}
fun Fragment.putArgs(args: BundleArgs) {
with(arguments) {
if (null == this) {
arguments = args.toBundle()
} else {
putArgs(args)
}
}
}
fun Bundle.putArgs(args: BundleArgs) {
putAll(args.toBundle())
}
// TODO: check: all kotlin `object`s that are put into `BundleArgs` must implement this interface
/**
* Used to keep the uniqueness of `object` when (serialize to)/(deserialize from) byte array,
* which is the current mechanism of [com.grindrapp.android.args.BundleArgs]
* */
interface SerializableKotlinObject: Serializable {
fun readResolve(): Any? {
return this::class.objectInstance/*singleton instance*/ ?: this
}
}
```
# Permission Util
```kotlin
//
// Copyright 2016 by Grindr LLC,
// All rights reserved.
//
// This software is confidential and proprietary information of
// Grindr LLC ("Confidential Information").
// You shall not disclose such Confidential Information and shall use
// it only in accordance with the terms of the license agreement
// you entered into with Grindr LLC.
//
package com.grindrapp.android.manager
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.grindrapp.android.AppSchedulers
import com.grindrapp.android.GrindrApplication
import com.grindrapp.android.extensions.resumeWhenActive
import io.reactivex.Observable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* Usages of ActivityResult family to ease our way to request permission or simply get result from an Activity.
* Ref: https://grindr.phacility.com/T27292
*/
fun ComponentActivity.isPermissionsGrantedRx(permission: Array<String>): Observable<Boolean> = Observable.create<Boolean> { emitter ->
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results ->
emitter.onNext(results.all { (_, granted) -> granted })
}.launch(permission)
}.subscribeOn(AppSchedulers.mainThread())
/**
* Suspend version for requesting permission and force to be triggered on main thread.
*
* @param permissions is the permisstion to request
*/
suspend fun ComponentActivity.isPermissionsGranted(permissions: Array<String>) = withContext(Dispatchers.Main) {
suspendCancellableCoroutine<Boolean> {
Timber.d { "chat/launch request in main" }
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results ->
it.resumeWhenActive(results.all { (_, granted) -> granted })
}.launch(permissions)
}
}
suspend fun Fragment.isPermissionsGranted(permissions: Array<String>) = withContext(Dispatchers.Main) {
suspendCancellableCoroutine<Boolean> {
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results ->
it.resumeWhenActive(results.all { (_, granted) -> granted })
}.launch(permissions)
}
}
suspend fun ComponentActivity.isPermissionGranted(permission: String) = isPermissionsGranted(arrayOf(permission))
object PermissionUtils {
val externalStoragePermissions = arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
val cameraPermissions = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
val locationPermissions = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
val videoCallPermissions = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
)
fun hasPermissions(permissions: Array<String>): Boolean {
return permissions.all { hasPermission(it) }
}
fun hasPermission(permission: String): Boolean {
val permissionCheck = ContextCompat.checkSelfPermission(GrindrApplication.application, permission)
return permissionCheck == PackageManager.PERMISSION_GRANTED
}
fun shouldShowRequestPermissionsRationale(activity: Activity, permissions: Array<String>): Boolean {
return permissions.any { ActivityCompat.shouldShowRequestPermissionRationale(activity, it) }
}
fun hasExternalStoragePermissions(): Boolean {
return hasPermissions(externalStoragePermissions)
}
fun hasLocationPermissions(): Boolean {
return hasPermissions(locationPermissions)
}
fun shouldShowRequestExternalStoragePermissionsRationale(activity: Activity): Boolean {
return shouldShowRequestPermissionsRationale(activity, externalStoragePermissions)
}
fun shouldShowRequestCameraPermissionsRationale(activity: Activity): Boolean {
return shouldShowRequestPermissionsRationale(activity, cameraPermissions)
}
fun openAppDetailsSettings(context: Context) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.fromParts("package", context.packageName, null)
context.startActivity(intent)
}
}
```
# VCS android studio resolve conflict
VCS -> git -> resolve conflict
# rebase
git rebase --onto master sha branchName
# Android Tools
https://plugins.jetbrains.com/plugin/8377-android-device-controller
# 改變 Build type directory
```
android {
.....
sourceSet {
dev {
}
}
}
```
Gradle 的 Android plugin 會將 對應的 flavor directory 底下的 code compile 進去 apk.
https://developer.android.com/studio/build/build-variants#sourcesets

# Android ANR
當 APP 發生 Exception 時,會交由此 Thread.UncaughtExceptionHandler 去殺掉 APP 的 Process,如果有自定義或者 Lib 中有定義 ExceptionHandler,**一定要在 uncaughtException 這個方法中 呼叫 Thread 預設的 UncaughtExceptionHandler**
https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/os/RuntimeInit.java;l=121;bpv=0;bpt=1
ANR 對於 "使用者按按鈕" 的 timeout 是 5 秒,當 Exception Handler 處理超過 5 秒就會觸發 Android 的 ANR 處理的機制.
https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=315?q=KEY_DISPATCHING_TIMEOUT
# Gradle
可以鎖定及查看整個專案的 依賴版本.
```
./gradlew :app:dependencies --write-lock
```
https://docs.gradle.org/current/userguide/dependency_locking.html
Remote 1m 1-2day
矩陣行
研發新產品(BU) 小團隊