Flipper

在继续实作 domain layer 之前,我们会介绍一个方便日常开发的工具:Flipper

Android Studio 有个功能是查看 HTTP request 和 UI layout,但有时不太方便。如果是查看 HTTP request 的话,有些人会用 proxy server 来截取 HTTP request 和 response。但有个问题是装置要先安装 proxy server 的 root certificate,而且部分 app 或 SDK 会做 cert pinning,驳了 proxy server 就用不到那些 app 或 SDK(Google Places SDK 会有这个问题)。

Flipper 的前身是 Stetho,或许大家以前有见过,就是 Facebook 借 Chrome DevTools 介面来提供 HTTP traffic、UI layout、Shared Preferences、SQLite database 查阅功能的那个 library。但因为用 Chrome DevTools 的界面做 UI,功能就会受到 DevTools 的限制,不能提供超越 DevTools 界面的功能。还有是 iOS 又不能用,app 结束後那个 DevTools 视窗就要作废不能重用。所以就促成 Facebook 开发 Flipper(前称 Sonar)。Flipper 是一个用 Electron 做的 desktop app 来做介面,并提供 Android 和 iOS SDK 把 app 的内容交予 Flipper desktop app。为了方便我们做 UI 时能看清楚 HTTP request,我们现在要做的是把 SDK 加到 app 入面。

首先是 app module 的 build.gradle 加上以下的 dependency:

debugImplementation "com.facebook.flipper:flipper:$flipperVersion"
debugImplementation "com.facebook.soloader:soloader:$soloaderVersion"
debugImplementation "com.facebook.flipper:flipper-network-plugin:$flipperVersion"

留意 Flipper Android SDK 在 Flipper 网站和 GitHub 的最新版本未必能在 Maven Central 找到,所以最好还是先检查 Maven Central 那边最新版本是甚麽

由於我们不想在 app 日後上架时都夹附 Flipper,我们就借用 build type 来控制:debug 才能用 Flipper;release 就不要有 Flipper 的 dependency。所以我们这次用 debugImplementation 而不是 implementation

接下来就是按照 Flipper 网站的指示Application class 的 onCreate 加上 Flipper 初始化的 code。不过我们应该还未有自己的 Application class,现在就先建立一个叫 EtaDemoAppApplication subclass。

@HiltAndroidApp
class EtaDemoApp : Application() {

    @Inject
    lateinit var flipperHelper: FlipperHelper

    override fun onCreate() {
        super.onCreate()
        flipperHelper.init()
    }
}

@HiltAndroidApp 是 Dagger Hilt 的 annotation,它是加在 Application 的 subclass。而 @Inject lateinit var flipperHelper 那句是叫 Dagger inject 一个 FlipperHelper 好让我们能在 onCreate 能用到它。这个 @Inject 的用法跟之前放在 constructor 时的用法不同,原因是 Android 的主要 class(例如 Application 和「四大组件」之称的 ActivityServiceContentProviderBroadcastReceiver)的 constructor 都是由 Android 系统去 call,Dagger 或其他 dependency injection library 就不能用 constructor injection 的方法把 dependency 提供给那些 class。取而代之是依靠系统 call 那些 onCreate 的 lifecycle callback 时你才有机会叫 dependency injection library 帮你拿到 dependency。所以我们改用 field injection 来取得 FlipperHelper。但你或许会觉得很神奇我们都没有在 onCreate call 到 Dagger 的 function 都能拿到 dependency。这是因为我们加了 @HiltAndroidAppEtaDemoApp。Dagger Hilt 的 Gradle plugin 在 compile app 时会帮我们重写一个新的 EtaDemoApp,里面的 onCreate 就会帮我们 call 了 Dagger 来做 field injection。意念上就像中国 Android 开发教学文章时常提及的 aspect-oriented programming (AOP)。(就是经常被拿来帮全个 app 的 OnClickListener 加插 event tracking code 那些教学)如果有用过未有 Dagger Hilt 之前的 Dagger,你会发现我们无写过甚麽 appComponent.inject(this) 之类的东西。因为 Dagger Hilt 替我们做了这些东西,那我们就可以专注写其他的 code,亦都令 Dagger 变得易用。

开了 EtaDemoApp 之後,同时亦要在 AndroidManifest.xmlapplication tag 加上 name attribute 指明要用 EtaDemoApp

<application
    android:name=".EtaDemoApp"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.ETADemo">
    <activity
        android:name=".MainActivity"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

你会看到 EtaDemoApp 入面用了一个 FlipperHelper ,这个 class 是我们现在会写的 class,我们就是把那些调用 AndroidFlipperClient 的东西全部塞进去而不是直接把那些 code 直接写进去EtaDemoApp 内。原因是我们刚在 build.gradle 设定只有在 debug build type 时才有 Flipper 的 dependency。如果直接把那些调用 AndroidFlipperClient code 放进去 EtaDemoApp 的话我们 build release build type 的 app 就会报错。解决方法是我们会在 debugrelease build type 的 src 目录各造一个 FlipperHelperdebug 那个 helper 就如 Flipper 网站示范的写法在 FlipperHelper.init call 一堆 AndroidFlipperClient 的 method 来初始化 Flipper;但在 release 那个 helper 就放个空白的 init 的 function 来令 compiler 成功 build 到 app。

Debug 部分

以下是 debug build type 的 FlipperHelper,它是放在 app/src/debug/java/net/swiftzer/etademo/flipper/FlipperHelper.kt。特别强调所放的位置是因为它要放在 debug build type 的 source directory。

class FlipperHelper @Inject constructor(
    @ApplicationContext private val context: Context,
    private val inspectorFlipperPlugin: InspectorFlipperPlugin,
    private val crashReporterPlugin: CrashReporterPlugin,
    private val databasesFlipperPlugin: DatabasesFlipperPlugin,
    private val sharedPreferencesFlipperPlugin: SharedPreferencesFlipperPlugin,
    private val networkFlipperPlugin: NetworkFlipperPlugin,
) {
    fun init() {
        SoLoader.init(context, false)
        if (!FlipperUtils.shouldEnableFlipper(context)) return
        val client: FlipperClient = AndroidFlipperClient.getInstance(context)
        client.addPlugin(inspectorFlipperPlugin)
        client.addPlugin(crashReporterPlugin)
        client.addPlugin(databasesFlipperPlugin)
        client.addPlugin(sharedPreferencesFlipperPlugin)
        client.addPlugin(networkFlipperPlugin)
        client.start()
    }
}

init 入面的东西基本上就是 Flipper 网站所写的内容,只是那些 plugin 换成经 Dagger 在 constructor inject 拿到的。留意 constructor 的 context 加了 @ApplicationContext,这个 annotation 是 Dagger Hilt 提供的。因为 context 有分 ActivityApplication,两个的 lifecycle 是不同的,所以你要指明你要的是那一个 context。这种用来告诉 Dagger 要 inject 相同 abstract class/interface 之下的那一款 subclass object 叫做 qualifier,我们稍後会再介绍。

由於我们打算用 Dagger inject 那些 Flipper plugin,所以我们要另外准备一个 Dagger module:FlipperDebugModule。叫它做 FlipperDebugModule 是因为这个 module 是放在 debug build type 的 source directory,它会放在 app/src/debug/java/net/swiftzer/etademo/flipper/FlipperDebugModule.kt

@Module
@InstallIn(SingletonComponent::class)
object FlipperDebugModule {

    @Provides
    fun provideInspectorFlipperPlugin(@ApplicationContext context: Context): InspectorFlipperPlugin =
        InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())

    @Provides
    fun provideCrashReporterPlugin(): CrashReporterPlugin = CrashReporterPlugin.getInstance()

    @Provides
    fun provideDatabasesFlipperPlugin(@ApplicationContext context: Context): DatabasesFlipperPlugin =
        DatabasesFlipperPlugin(context)

    @Provides
    fun provideSharedPreferencesFlipperPlugin(@ApplicationContext context: Context): SharedPreferencesFlipperPlugin =
        SharedPreferencesFlipperPlugin(context)

    @Provides
    @Singleton
    fun provideNetworkFlipperPlugin(): NetworkFlipperPlugin = NetworkFlipperPlugin()

    @Provides
    @FlipperInterceptor
    fun provideFlipperInterceptor(networkFlipperPlugin: NetworkFlipperPlugin): Interceptor =
        FlipperOkhttpInterceptor(networkFlipperPlugin)
}

Flipper plugin 这个部分或许不用 Dagger 来 inject 亦可以,但因为我们需要为 Ktor client 所用的 OkHttp client 加插 FlipperOkhttpInterceptor 才能在 Flipper desktop app 看到 HTTP traffic,所以还是最少需要把 NetworkFlipperPluginFlipperOkhttpInterceptor 纳入 Dagger dependency graph 内。这次我们用 object 来做 FlipperDebugModule 是因为它只会放 @Provides function,没有 abstract function,所以全部 function 都是 static 效能较佳。

留意 provideFlipperInterceptor return type 是 OkHttp 的 Interceptor,这是因为我们不想把 Flipper 的 class 外露给其他地方知道,这样 release build type 就能顺利地 compile。另一个要留意是我们加了 @FlipperInterceptor,这个是我们自制的 Dagger qualifier annotation。虽然这个 app 应该不会再有其他 OkHttp interceptor,但在现实中的 app 有时会有多於一个 interceptor,所以顺带示范了 qualifier 的用法。FlipperInterceptor 放在 data package 内,因为跟网路相关。这个就不用分 build type 放,放在 main 的 source directory 就可以了。

@Qualifier
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class FlipperInterceptor

如果日後再有其他 OkHttp interceptor,那就照样加一个新的 qualifier annotation 标记它。

另一样要留意是 provideNetworkFlipperPlugin 我们加了 @Singleton。这是因为没有加 @Singleton 的话每次一有地方要用到 NetworkFlipperPlugin Dagger 就会 call 你写的 provideNetworkFlipperPlugin 来 instantiate 一个全新的 NetworkFlipperPlugin。但这样的话你就不可能在 Flipper desktop app 看到 HTTP traffic。 这是因为 FlipperHelper.init 所用的 NetworkFlipperPlugin 和加插在 OkHttp 的 FlipperOkhttpInterceptor 所用的 NetworkFlipperPlugin 是两个完全不同的 instance。 为了令两者所用的 NetworkFlipperPlugin 都是同一个 instance,所以就加了 @Singleton 这个 scope。在 Dagger Hilt 的定义下,@Singleton 的范围就是 Application class 的存活范围。由於在 Android 下 Application class 只有一个 instance,所以可以理解为整个 app 最多只有一个 NetworkFlipperPlugin 的 instance。

回到 DataModule,我们要在 provideOkHttpClient 加插 FlipperOkhttpInterceptor。由於在 release build type 时整个 app 都不会出现 provideFlipperInterceptor,所以我们会好像之前 logger 般把它包成 Optional

@Provides
@Singleton
fun provideOkHttpClient(@FlipperInterceptor flipperInterceptor: Optional<Interceptor>): OkHttpClient {
    val builder = OkHttpClient.Builder()
    flipperInterceptor.ifPresent { builder.addNetworkInterceptor(it) }
    return builder.build()
}

由於加了 Optional ,我们亦都要为它补上 @BindsOptionalOf

@Module
@InstallIn(SingletonComponent::class)
interface DataModule {
    @BindsOptionalOf
    @FlipperInterceptor
    fun bindFlipperInterceptor(): Interceptor

    // ...
}

留意我们每次用到 Interceptor 都要加上 @FlipperInterceptor,否则 Dagger 不会在 dependency graph 找到这个 Interceptor

最後就是为 debug build type 加一个 activity,它是放在 app/src/debug/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.swiftzer.etademo">

    <application>
        <activity
            android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
            android:exported="true" />
    </application>
</manifest>

Release 部分

由於 Flipper 只会在 debug build type 才会用到,换到 release 的部分我们只需要做个空白的 FlipperHelper 满足 compiler 的要求就可以了。这个 FlipperHelper 要放在 app/src/release/java/net/swiftzer/etademo/flipper/FlipperHelper.kt

class FlipperHelper @Inject constructor() {
    fun init() {
        // No-op
    }
}

由於这个 FlipperHelper 没有在 constructor 用到那些 Flipper plugin,所以我们就不用写一个 FlipperReleaseModule 之类的东西,就是这麽简单。


你或许会问为甚麽我们不乾脆把 EtaDemoAppdebug release 两个版本而要另外开一个 FlipperHelper 分两边放。这是因为 application class 通常都会有其他东西,为了一个 Flipper 而要维护两个(或更多个)application class 是费时失事。如果 build variant 再增加下去的话,我会建议另开两个 Gradle module 放有 Flipper 和无 Flipper 版的 FlipperHelper ,然後再 xxxImplementation project(':flipper')project(':flipper-noop') 这样。其实 Flipper 本身都有提供 no-op 版 artifact,但不是所有 Flipper plugin 都有提供 no-op 版 artifact,所以还在在本篇示范了如何自行做 no-op。

小结

我们现在已经加了 Flipper,它的 network 功能我们之後会用到(我们要看它有没有定时自动更新班次)。为甚麽要用一篇文章写 Flipper 呢?这是因为以前工作时经常被 backend 同事问到 Android app 如何 call 那个 endpoint,最直观的方法就是用 proxy 或者 Flipper 这类工具先看看 network traffic 然後才在 app 的 source code 找 call 那个 endpoint 的位置。这样我就不用把 code 由头看到尾,还有是 backend 同事能自己直接试,不用再走来问我这页 call 了甚麽 endpoint、payload 是甚麽之类的问题。

当然,如果要做到改 response、延迟 request/response 这些功能的话,还是需要用到 proxy server。我平常用开的 proxy server 是 Whistle,proxy server 还有其他选择,例如 CharlesProxymanFiddler 等等。

另外亦借安装 Flipper 介绍了 Dagger Hilt 比 Dagger 简化了甚麽地方,Dagger 的 scope 和 qualifier 用法,还有是 build type 的用法。

这次示范的完整 code 可以在 GitHub repo 找到。

参考


<<:  Proxmox VE 虚拟机防火墙管理 (二)

>>:  Day15:15 - 购物车服务(3) - 後端 - 购物车数量增减、删除API

CSS display:grid

Gird是一种二维的布局方式,相较flex来说grid还多控制了列~ example : <d...

Vpn架设

我家使用的是大大宽频 没有固定ip 最近购入一台totolink的路由器想要架设vpn 但是设定完了...

Day21 - 前处理: 语者正规化

前一天在说明使用的语音特徵时有提到,模型有静态模型跟动态模型两种。在训练静态模型时,因为资料集中的语...

Day27:今天我们来聊一下将Microsoft 365 Defender 连接到 Azure Sentinel

Microsoft 365 security portal提供目标导向的使用者介面,以降低Micro...

WIN 10 看不到WIFI

Q : wi10 看不到wifi 在cmd 输入 netsh wlan set hostednetw...