Android Weekly Notes Issue #428

Android Weekly Issue #428

Kotlin Flow Retry Operator with Exponential Backoff Delay

這是講協程Flow系列文章中的一篇.

對於重試的兩個操作符:

  • retryWhen
  • retry

retryWhen的使用:

.retryWhen { cause, attempt ->
    if (cause is IOException && attempt < 3) {
        delay(2000)
        return@retryWhen true
    } else {
        return@retryWhen false
    }
}

retry:

.retry(retries = 3) { cause ->
    if (cause is IOException) {
        delay(2000)
        return@retry true
    } else {
        return@retry false
    }
}

可以把時間指數延長:

viewModelScope.launch {
    var currentDelay = 1000L
    val delayFactor = 2
    doLongRunningTask()
        .flowOn(Dispatchers.Default)
        .retry(retries = 3) { cause ->
            if (cause is IOException) {
                delay(currentDelay)
                currentDelay = (currentDelay * delayFactor)
                return@retry true
            } else {
                return@retry false
            }
        }
        .catch {
            // error
        }
        .collect {
            // success
        }
}

Fragments: Rebuilding the Internals

Fragment在Android 10已經廢棄, 現在不在framework中了, 只在AndroidX中有.

這個Fragment 1.3.0-alpha08版本的發佈, 有一些關於FragmentManager內部狀態的重要更新.
解決了很多issue, 簡化了fragment的生命周期, 還提供了一個FragmentManager多個back stacks的支持.

核心就是這個FragmentStateManager類.

這個FragmentStateManager負責:

  • 轉換Fragment的生命周期狀態.
  • 跑動畫和轉場.
  • 處理延遲轉換.

Postponed fragments

關於狀態的確定, 有一個case是一個難點: postponed fragments.
這是一個以前就有的東西, 通常跟shared element transition動畫有關係.

postponed fragment有兩個特點:

  • view創建了, 但是不可見.
  • lifecycle頂多到STARTED.

只有調用這個方法: startPostponedEnterTransition()之後, fragment的transition才會跑, view會變成可見, fragment會移動到RESUMED.

所以有這個bug: Postponed Fragments leave the Fragments and FragmentManager in an inconsistent state bug.

這個issue相關聯的還有好幾個issues.

在容器層面解決問題

用一個SpecialEffectsController(以後名字可能會改)來處理所有動畫轉場相關的東西.

這樣FragmentManager就被解放出來, 不需要處理postponed的邏輯, 而是交給了container, 這樣就避免了FragmentManager中狀態不一致的問題.

新的StateManager構架

原先: 一個FragmentManager總管所有.

現在: FragmentManager和各個FragmentStateManager的實例交流.

  • The FragmentManager only has state that applies to all fragments.
  • The FragmentStateManager manages the state at the fragment level.
  • The SpecialEffectsController manages the state at the container level.

總體

這個改動新發佈, 實驗階段, 總體來說是應該沒有行為改變的.

如果有行為改變, 對你的程序造成了影響, 也可以暫時關閉(FragmentManager.enableNewStateManager(false)), 並且報告個issue.

A Framework For Speedy and Scalable Development Of Android UI Tests

講了一整套的測試實踐.

沒有用Appium, 用的UI Automator和Espresso.

Basic Coroutine Level 1

Kotlin協程的概念.

Android Lint Framework — An Introduction

Android Lint的介紹.

創建一個Lint規則, 保證每個人都用項目自定義的ImageView, 而不是原生的ImageView.

具體做法:

  • 首先從創建一個叫做custom-lint的module. 需要依賴lint-apilint-checks:
compileOnly "com.android.tools.lint:lint-api:$androidToolsVersion"
compileOnly "com.android.tools.lint:lint-checks:$androidToolsVersion"

這裡用了compileOnly是因為不想lint API在runtime available.

  • 之後創建自定義規則. 每個lint check的實現都叫一個detector. 需要繼承Detector, 並且利用Scanners來做掃描. 報告錯誤需要定義Issue. 還可以創建LintFx, 作為quick fix.
class ImageViewUsageDetector : LayoutDetector() {
  
   // Applicable elements
  
  
    override fun visitElement(context: XmlContext, element: Element) {
        context.report(
            issue = ISSUE, 
            location = context.getElementLocation(element), 
            message = REPORT_MESSAGE, 
            quickfixData = computeQuickFix() 
        )
    }

   
    private fun computeQuickFix(): LintFix {
        return LintFix.create()
            .replace().text(SdkConstants.IMAGE_VIEW)
            .with(TINTED_IMAGE_VIEW)
            .build()
    }

  // Issue, implementation, and other constants 

}
  • 然後把定義好的自定義規則註冊.
class Registry: IssueRegistry() {

    override val issues: List<Issue>
        get() = listOf(ImageViewUsageDetector.ISSUE)

    override val api: Int = CURRENT_API

}
  • 創建入口, 在build.gradle文件中:

// Configure jar to register our lint registry
jar {
    manifest {
        attributes("Lint-Registry-v2": "com.tintedimagelint.lint.Registry")
    }
}
  • 加上依賴和一些配置.

android {
    
    // Configurations above
    lintOptions {
        lintConfig file('../analysis/lint/lint.xml')
        htmlOutput file("$project.buildDir/reports/lint/lint-reports.html")
        xmlOutput file("$project.buildDir/reports/lint/lint-reports.xml")
        abortOnError false
    }
    //Configurations below
}

dependencies {
    // Dependencies above

    // Include custom lint module as a lintCheck
    lintChecks project(":custom-lint")

    // Dependencies below
}

Codelabs for new Android game technologies

關於Android Game新技術的Codelabs:

都是Unity的game.

Android Vitals – When did my app start?

系列文章之六, 我的app啥時候啟動的?

看個結論吧:

Here’s how we can most accurately measure the app start time when monitoring cold start:

  • Up to API 24: Use the class load time of a content provider.
  • API 24 – API 28: Use Process.getStartUptimeMillis().
  • API 28 and beyond: Use Process.getStartUptimeMillis() but filter out weird values (e.g. more than 1 min to get to Application.onCreate()) and fallback to the time ContentProvider.onCreate() is called.

Comparing Three Dependency Injection Solutions

比較三種依賴注入的解決方案.

  • 手寫方式.
  • Koin.
  • Dagger Hilt.

Avoiding memory leaks when using Data Binding and View Binding

使用Data Binding和View Binding的時候, 注意內存泄漏問題.

Google建議在Fragment中使用binding時, 要在onDestroyView中置為null:

private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    _binding = ResultProfileBinding.inflate(inflater, container, false)
    val view = binding.root
    return view
}

override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
}

有個博客中介紹的方法, 可以簡化成這樣:

private val binding: FragmentFirstBinding by viewBinding()

Fragment還有一個參數的構造, 可以傳入布局id:

class FirstFragment : Fragment(R.layout.fragment_first) {

  private val binding: FragmentFirstBinding by viewBinding()
  
  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
      super.onViewCreated(view, savedInstanceState)
      // Any code we used to do in onCreateView can go here instead
  }
}

冷知識: DataBinding實現了ViewBinding.

public abstract class ViewDataBinding extends BaseObservable implements ViewBinding

所以ViewBinding和DataBinding方法通用.

Anti-patterns of automated software testing

關於測試的一些anti-patterns.

推薦閱讀.

Using bytecode analysis to find unused dependencies

關於這個庫: //github.com/autonomousapps/dependency-analysis-android-gradle-plugin的說明.

Code

後記

好久沒在博客園發過這個系列.
其實一直還有在更, 只不過寫得比較散亂隨意, 所以丟在了簡書:
//www.jianshu.com/c/e51d4d597637

最近有點忙, 不太有時間寫博客, 積攢了好多話題都是沒有完成的.
看博客兩個月沒更了, 拿這篇刷一下存在感.
是想多寫點真正厲害有價值的原創的.
先韜光養晦, 積累一下.