# 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. ![](https://i.imgur.com/iWBSOJC.png) 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. ![](https://i.imgur.com/f4SlaB0.png) 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? ![](https://i.imgur.com/Urpw7m2.png) ## 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 ![](https://i.imgur.com/pCvqy3Q.png) ## Clean Architecture ![](https://i.imgur.com/wL60T2T.png) ## 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 ![](https://i.imgur.com/IYzWTSf.png) ## 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 ![](https://i.imgur.com/uUl2QtU.png) 與 Super View 合作去 measure 大小的關係表 lp 代表的是 LayoutParams ![](https://i.imgur.com/pWGjwuy.png) # 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. ![](https://i.imgur.com/owU3wyU.png) # 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 讀取。 ![](https://i.imgur.com/SkhwgSI.png) Thread ![](https://i.imgur.com/lQX2ijO.png) 生產者消費者 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 條件 (自動換行) ![](https://i.imgur.com/eg11EhS.png) # 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 注意事項 ![](https://i.imgur.com/QEWPwdG.png) # Android 注意事項 ![](https://i.imgur.com/jcqKNts.png) ![](https://i.imgur.com/wHdE5Dq.png) # 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 ![](https://i.imgur.com/ipHNd8x.png) ## Find Java location. If you don't want to install java, just use Android Studio Java. ![](https://i.imgur.com/n8PPNdC.png) ## Update Tools in Linux 1. First, update apt-get 2. Second, apt upgrade "tools name" ![](https://i.imgur.com/wiz5oax.png) # 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 ![](https://i.imgur.com/8nDaGD6.png) # 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) 小團隊