用 Androidx 玩转 HelloWorld:Git/Github,MVVM+LiveData,Hilt,SharedPreference,Espresso 和 中英互换 大锅饭。

本文通过一个Android应用实例,介绍了如何使用Git进行版本控制,实现MVVM架构,结合Hilt进行依赖注入,以及使用SharedPreference存储数据。同时,文章详细讲解了Espresso测试和国际化切换的过程,帮助开发者提升开发效率和应用质量。

👋1. 云后备介绍和 Git

😀程序员嘛,当然要有后备啦。Git 是其中云后备的一种。
👶🏻为啥要加这个家伙呢,Ctrl+Z 不就搞定了吗?
🙀那怎么可能呢!Ctrl+Z是有限的,打着打着就回不去了。要吧死机了,停电,手提被偷了,后备进水了, 要多衰有多衰,足额衰神上身。你能重来吗?噢嘛呢吧咪哄?(🔈 🔉 🔊)
👶🏻不会吧,这是我吗?你是卖保险的吗?
👴🏻这不有云后备嘛!每个新手都要有,很好用,想怎么倒就怎么倒。Git 是其中一个老牌啦,上 Github 开个户口,不要钱,老子喜欢免费的。你会不会啊?不会的出门转左有个模仿塑像的。
👱🏻是啊,在黑苹果上, XCode 12 开头就要你加,我现在学 IOS,正一头雾水呢。还是拿个 Android Studio 示范示范吧。


👉2. 安卓开个新玩家 Git Practice

图像活动
empty打开 Android Studio, 开个空的活动 。
Git Practice就是 Git Practice 啦!
😄Package name 用自己的名字。

👀3. 加 Git

趁现在还没划地盘,把 Git 加上去。👶🏻:刚开始了,就加啦?👱🏻:对啊,省得忘了,人一干起来,就天昏地暗地。
下载 Git: Git 下载

settings装 Git 之后,在 Android Studio 打开 设定 Settings
Git跳到下面 Version Control
=> Git
merge在 Update method,
方式选 Merge 。

找Git
按 Test,让自己找对象啦。
重启,回来用终端 Terminal,看看能用否?
Git version


🔑4. 加 GitHub

到 GitHub 开户口: Github

【🔑】拿钥匙

settings在 GitHub 内,
右上角
developer左边目录,
选Developer Settings
在这里插入图片描述左边目录,
Personal access tokens
私人钥匙
genGenerate new token
设定新钥匙
Note干啥的?留个记录
资料库资料库权限
在这里插入图片描述管理员组织权限:
主要是读的那个
Share分享权
key建立

my key

【🔑】插钥匙

👶🏻:呕也,有钥匙啦!。。。咋用呢?
😇:回 Android Studio,打开 设定Settings

VC->Github这次下到 Version Control => Github。
Token插钥匙。
在这里插入图片描述你要留意它的三大要点。
Add Account加户口。
在这里插入图片描述我名字有了。
你的呢?

【🔑】上传文档

😷OK,完工。哦,还没有,要把这份项目上传。

VCSEnable Version Control
开启版本控制
option谷歌只安了三个,其他的要自己加。
commit上传
select管他呢,全加去。
note做个记号。Commit上传。
commit again👶:有问题啊!
🤓:正常啦,继续Commit上传。

【🔑】 .gitignore

第一次总算干完了,看看多了啥。
dir
外边多了个 .gitignore 文件。

*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

🤖:里面装的都是不用上传的文件,建议你把任何秘密文件都加进去,不然你知我知大家都知。你都不希望见到老板这个样子:🤬。


🍛5. MVVM 划地盘, 加 Gradle

现在,做移动都要先划地盘。苹果都是老牌MVC,而安卓已经改成MVVM。倚天屠龙,谁与挣疯…风?
👶:啥叫划地盘啦?
😎:就是把编码划分成几个文件夹啦。
我是这样划的:
mvvm
先这样,来点简单的。
加 Gradle 的资料库文件:
改 Project: 加 Dagger-Hilt
Projec

buildscript {
    ext.kotlin_version = "1.4.20"
    repositories {
        google()
        jcenter()
        mavenCentral()
        maven { url "https://oss.jfrog.org/libs-snapshot" }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:4.2.0-alpha15'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        // Hilt
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}
...

Module:
module

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-android-extensions'
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}
android {
    compileSdkVersion 30
    ...
    buildFeatures {
        viewBinding true
    }
}
dependencies {
    // Test
    testImplementation 'junit:junit:4.13'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    // Assertions
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.ext:truth:1.3.0'
    androidTestImplementation 'com.google.truth:truth:1.0'

    // STD
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    // Design
    implementation 'com.google.android.material:material:1.2.1'
    // Layout
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'

    // Hilt
    def hilt_version = '2.28-alpha'
    def hilt_lifecycle_version = '1.0.0-alpha02'
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
    implementation "androidx.hilt:hilt-lifecycle-viewmodel:$hilt_lifecycle_version"
    kapt "androidx.hilt:hilt-compiler:$hilt_lifecycle_version"
    // Tests
    androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
    kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"

    // Lifecycle
    def lifecycle_version = "2.2.0"
    // ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
    // LiveData
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
    // Lifecycles only (without ViewModel or LiveData)
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
    // Test helpers for LiveData
    def arch_version = "2.1.0"
    testImplementation "androidx.arch.core:core-testing:$arch_version"

    // Activity and Fragment
    def activity_version = "1.2.0-beta01"
    def fragment_version = "1.3.0-beta01"
    implementation "androidx.activity:activity-ktx:$activity_version"
    implementation "androidx.fragment:fragment-ktx:$fragment_version"
    // Testing Fragments in Isolation
    debugImplementation "androidx.fragment:fragment-testing:$fragment_version"
}

全部换成 androidx 版的, 因此要改 gradle.properties,加入:

...
android.useAndroidX=true
android.enableJetifier=true

Sync 同步。Ctrl + K:
commit
叫 Gradle 吧,传上去再说。


🌏6. 改个【中英双语 】版

🤔先加个中文版面吧,以后的项目都可以抄这个。

[ 🀄️ ] 中文资源

add resource新资源
locale中文版

res/values/strings 文件夹就了一个 strings.xml:
strings.xml

[ 🀄️ ] activity_main.xml 编辑

在这里插入图片描述
练习只要简单的, 只有一个文条和浮动按键。Layout 要改成:

<androidx.constraintlayout.widget.ConstraintLayout

IDE 里面的 FloatingActionButton 不好用,用私货:CoordinatorLayout+TextView。
localeFab:CoordinatorLayout 浮动按键
changeTV:TextView 按键文字
这样构图方便改,你可以把组合加捷径到 Live Template 的 XML 组。

所有 android:text="…" 选项都用 Alt+Enter 储存在 strings.xml 里面,当你换语言的时候可以一次更换。

[ 🀄️ ] 翻译器,加中文。

选探索(😅横幅最后那个,又搜上了,和 VS Code 差不多。)
search
输入: edit
editor
加入中文文本啦:
translate


📥7. Hilt 注射,比它老爸 Dagger🔪 简单

加个 Application(头,应用,随你叫):在 application 文件夹。

@HiltAndroidApp
class GitPracticeApp : Application()

当然,新头要加进 AndroidManifest.xml:

    <application
        android:name=".application.GitPracticeApp" 
        android:configChanges="locale"
        。。。

然后 Activity 和 Fragment 的 class 前面要加

@AndroidEntryPoint

ViewModel 可以用 @ViewModelInject 直接注射,例如:

class BaseViewModel @ViewModelInject constructor(
    private val storage: Storage
) : ViewModel()  {

其它地方可以用 @Singleton@Inject 注射,例如:

@Singleton
class FoodRepository @Inject constructor(
    private val storage: Storage
) {

【 Hilt 插入Storage 】

Storage是内部储存(SharedPreference),我用来储存使用过的语言。分成两拨,一个是资源,一个注射器。
storage
资源:

【 Storage 】界面

Storage.kt

interface Storage {
    fun setString(key: String, value: String)
    fun getString(key: String): String

    fun setBoolean(key: String, value: Boolean)
    fun getBoolean(key: String): Boolean

    fun setInt(key: String, value: Int)
    fun getInt(key: String): Int

    fun delKey(key: String): Boolean
    fun clear()
}

【 SharedPreferencesStorage 】样板戏

SharedPreferencesStorage.kt:老样子。

class SharedPreferencesStorage @Inject constructor(
    @ApplicationContext context: Context
) : Storage {

    private val fileName = "gitPractice"

    private val sharedPreferences = context
        .getSharedPreferences(fileName, Context.MODE_PRIVATE)

    // String Value
    override fun setString(key: String, value: String) {
        with(sharedPreferences.edit()) {
            putString(key, value)
            apply()
        }
    }

    override fun getString(key: String): String {
        return sharedPreferences.getString(key, UNKNOWN)!!
    }

    // Boolean Value
    override fun setBoolean(key: String, value: Boolean) {
        with(sharedPreferences.edit()) {
            putBoolean(key, value)
            apply()
        }
    }

    override fun getBoolean(key: String): Boolean {
        return sharedPreferences.getBoolean(key, false)
    }

    // Integer Value
    override fun setInt(key: String, value: Int) {
        with(sharedPreferences.edit()) {
            putInt(key, value)
            apply()
        }
    }

    override fun getInt(key: String): Int {
        return sharedPreferences.getInt(key, 0)
    }

    // Remove value
    override fun delKey(key: String): Boolean {
        with(sharedPreferences.edit()) {
            remove(key)
            apply()
        }
        return !sharedPreferences.contains(key)
    }

    // Clear all data
    override fun clear() {
        with(sharedPreferences.edit()) {
            clear()
            apply()
        }
    }
}

然后,就可以注射了:

【 StorageModule 】注射药

StorageModule.kt:Hilt 继承了 Dagger 的传统,@Binds 跟 abstract,其它用 @Provides。不过已经简化了很多,都不用写 Component 了。

@InstallIn(ApplicationComponent::class)
@Module
abstract class StorageModule {
    @Binds
    abstract fun provideStorage(
         storage: SharedPreferencesStorage
    ): Storage
}

🏹8. 注射 Storage 进 ViewModel

MVVM 嘛,Activity 当然有 ViewModel 做伙伴啦。

{ BaseActivity }

我这用 BaseActivity.kt

Configuration 🤠 向 Context 🤯 鞠躬:“大哥,这是我的小弟, (Locale 😔)。旗人,改头换面的事,交代他干就行。”
Context 🤯: 不错,我要中文。
Locale 😔:我变。
Context 🤯:我要英文。
Locale 😔:I’m here。
Context 🤯:我要日文。
Locale 😲:大大哥,没有啊!变不了,没马甲啊。你瞧:

@Suppress("DEPRECATION")
@AndroidEntryPoint
abstract class BaseActivity : AppCompatActivity() {

    private var mCurrentLocale: Locale? = null
    private val baseVM: BaseViewModel by viewModels()

    override fun onStart() {
        super.onStart()

        // Set default locale
        val mLocale = baseVM.setDefault()
        changeLocale(this, mLocale)
        mCurrentLocale = resources.configuration.locale
        lgd("BaseAct: Locale: $mCurrentLocale")
    }

    override fun onRestart() {
        super.onRestart()
        val locale = baseVM.getDefaultLocale()
        if (locale != mCurrentLocale) {
            mCurrentLocale = locale
            recreate()
        }
    }

    override fun applyOverrideConfiguration(overrideConfiguration: Configuration?) {
        if (overrideConfiguration != null) {
            val uiMode = overrideConfiguration.uiMode
            overrideConfiguration.setTo(baseContext.resources.configuration)
            overrideConfiguration.uiMode = uiMode
        }
        super.applyOverrideConfiguration(overrideConfiguration)
    }

    fun changeLocale(context: Context, locale: Locale?) {
        lgd("BaseAct: changeLocale() ================> $locale")
        val res: Resources = context.resources
        val conf: Configuration = res.configuration
        conf.setLocale(locale)
        baseContext.resources.updateConfiguration(conf, baseContext.resources.displayMetrics)
    }
}

简单,只是换语言 locale。不懂的地方看 【第 9 章】。

{ BaseViewModel }

BaseActivity 的 ViewModel – BaseViewModel.kt:

class BaseViewModel @ViewModelInject constructor(
    private val storage: Storage
) : ViewModel()  {

    private var mLocale: Locale? = null

    fun setDefault(): Locale {
        lgd("BaseVM: setDefault()")
        mLocale = getLocale(storage)
        return mLocale as Locale
    }

    fun getDefaultLocale(): Locale? {
        lgd("BaseVM: getDefaultLocale()")
        return mLocale
    }
}

🉑9. 加个通用文件夹 util,大家合用。

util

《 LocaleHelper 》

getLocale() 来自 util/LocaleHelper.kt
里面只放一个量:用过什么语言(中文为第一选择)。

val Lang_Chinese: Locale = Locale.CHINA
val Lang_US_English: Locale = Locale.US

fun getLocale(storage: Storage): Locale {
    lgd("LocaleHelper: getLocale()")
    val language = storage.getString(LANGUAGE)

    var locale: Locale? = null
    when (language) {
        UNKNOWN -> {
            lgd("LocaleHelper: 无记录,中文为第一选择。")
            storage.setString(LANGUAGE, CHINA)
            locale = Lang_Chinese
        }
        CHINA -> locale = Lang_Chinese
        US -> locale = Lang_US_English
    }
    return locale!!
}

《 Config 》

util 文件夹还有两个文件:
Config.kt:装通用的 String 文本代码

// Storage
const val UNKNOWN = "UNKNOWN"
const val CHINA = "China"
const val US = "United State"
const val LANGUAGE = "Language"

《 LogHelper 》

LogHelper.kt:看Logger,查错用的。

const val TAG = "MLOG"
fun lgd(s:String) = Log.d(TAG, s)
fun lgi(s:String) = Log.i(TAG, s)
fun lge(s:String) = Log.e(TAG, s)

你要在下面 Logcat 加过滤
filter
Mlog
这样你就可以直接看 标有 MLOG 的记录。


📱10. MainActivity 和 ViewModel

( MainActivity )

MainActivity.kt :这个不用加 @AndroidEntryPoint ,因为 BaseActivity 有。如果爸爸的爸爸有那么爸爸也不用加,有点绕口啊。

class MainActivity : BaseActivity() {

    // ViewModel
    private val mainVM: MainViewModel by viewModels()

    val localeFab: CoordinatorLayout by lazy { findViewById(R.id.localeFab) }
    val titleTV: TextView by lazy { findViewById(R.id.titleTV) }
    val changeTV: TextView by lazy { findViewById(R.id.changeTV) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        localeFab.setOnClickListener {
            lgd("MainAct: Clicked")
            mainVM.swapLocale()
        }

        mainVM.currentLocale.observe(
            this,
            { mLocale ->
                lgd("MainAct: Observable Language => $mLocale ")
                when (mLocale) {
                    CHINA -> {
                        lgd("MainAct: Selected China...")
                        changeLocale(this, Locale.CHINA)
                    }
                    US -> {
                        lgd("MainAct: Selected English(US)...")
                        changeLocale( this, Locale.US)
                    }
                }
            }
        )
    }


}

“by lazy” 挺好用,下面不用再写一堆东西。LiveData 在 ViewModel 里面,你要连着看。被监视的对象是 currentLocale

( MainViewModel )

MainViewModel.kt

class MainViewModel @ViewModelInject constructor(
    private val storage: Storage
) : ViewModel()  {

    val currentLocale = MutableLiveData<String>()

    init {
        // storage.clear()  // 测试启动无资料时用的,可以删掉。
        when (getLocale(storage)) {
            Locale.CHINA-> currentLocale.value = CHINA
            Locale.US-> currentLocale.value = US
        }
    }

    fun swapLocale() {
        val language = storage.getString(LANGUAGE)
        lgd("MainVM: Current language = $language")

        when (language) {
            CHINA -> {
                storage.setString(LANGUAGE, US)
                currentLocale.postValue(US)
            }
            US ->  {
                storage.setString(LANGUAGE, CHINA)
                currentLocale.postValue(CHINA)
            }
        }
    }
}

看,写着写着就忘了上传 GitHub,这是坏习惯,赶紧 Ctrl+K :
commit base


⛑11. Logcat:找错 、加速。

Run 跑跑看,切,没用没有中文。看看下面 Logcat:

D: LocaleHelper: getLocale()
D: LocaleHelper: 无记录,中文为第一选择。
D: BaseVM: setDefault()
D: LocaleHelper: getLocale()
D: BaseAct: changeLocale() ================> zh_CN      《==     一次
D: BaseAct: Locale: zh_CN
D: MainAct: Observable Language => China 
D: MainAct: Selected China...
D: BaseAct: changeLocale() ================> zh_CN      《==     两次
D: MainAct: Clicked
D: MainVM: Current language = China
D: MainAct: Observable Language => United State 
D: MainAct: Selected English(US)...
D: BaseAct: changeLocale() ================> en_US
D: MainAct: Clicked
D: MainVM: Current language = United State
D: MainAct: Observable Language => China 
D: MainAct: Selected China...
D: BaseAct: changeLocale() ================> zh_CN

开头 第 5 行 和 第 10 行 重复 了,要组织一下他们次序。Androidx 有点不一样,不会主动更新,看来要手动刷版。
先看 BaseActivity:

    override fun onStart() {
        super.onStart()

        // Set default locale
        val mLocale = baseVM.setDefault()      //  这里输送方向错了
        changeLocale(this, mLocale)            //  重复
        mCurrentLocale = resources.configuration.locale
        lgd("BaseAct: Locale: $mCurrentLocale")
    }
  • MVVM 输送的方向是 单向 的,删除返回项目两个。
  • 下面那个没有必要,可以直接在 ViewModel 内启动。

结果:

    override fun onStart() {
        super.onStart()
        mCurrentLocale = resources.configuration.locale
        lgd("BaseAct: Locale: $mCurrentLocale")
    }

    override fun onRestart() {
        super.onRestart()
        val locale = baseVM.mLocale
        if (locale != mCurrentLocale) {
            mCurrentLocale = locale
            recreate()
        }
    }

BaseViewModel:setDefault 取消返回;增加开启项 init() 。

    var mLocale: Locale? = null

    init {
        setDefault()
    }

    fun setDefault() {
        lgd("BaseVM: setDefault()")
        mLocale = getLocale(storage)
    }

MainActivity:增加手动刷新。

override fun onCreate(savedInstanceState: Bundle?) {
        。。。

        mainVM.currentLocale.observe(
            this,
            { mLocale ->
                lgd("MainAct: Observable Language => $mLocale ")
                when (mLocale) {
                    CHINA -> {
                        lgd("MainAct: Selected China...")
                        changeLocale(this, Locale.CHINA)
                        updateText() // 《 《 《 《 《 《 《 《 《 增加刷新 
                    }
                    US -> {
                        lgd("MainAct: Selected English(US)...")
                        changeLocale(this, Locale.US)
                        updateText() // 《 《 《 《 《 《 《 《 《 增加刷新 
                    }
                }
            }
        )
    }

    private fun updateText() { // 《 《 《 《 《 《 《 《 《 手动刷新 
        lgd("MainAct: 更新版面, updateText().")
        val changeText = getString(R.string.change_language)
        changeTV.text = changeText
        val helloText = getString(R.string.helloworld)
        titleTV.text = helloText
        val actionText = getString(R.string.app_name)
        supportActionBar?.title = actionText
        lgd("MainAct: Language swap: $changeText, $helloText, $actionText ")
    }

Ctrl+K,上传 GitHub。
update
Run,再跑。
result
看看 Logcat :

D: LocaleHelper: getLocale()
D: LocaleHelper: 无记录,中文为第一选择。
D: BaseAct: Locale: en_US
D: MainAct: Observable Language => China 
D: MainAct: Selected China...
D: BaseAct: changeLocale() ================> zh_CN
D: MainAct: 更新版面, updateText().
D: MainAct: Language swap: => 英语, 向大家问好!, Git 后备管理 
D: MainAct: Clicked
D: MainVM: Current language = China
D: MainAct: Observable Language => United State 
D: MainAct: Selected English(US)...
D: BaseAct: changeLocale() ================> en_US
D: MainAct: 更新版面, updateText().
D: MainAct: Language swap: => Chinese, Hello World!, Git Practice 
D: MainAct: Clicked
D: MainVM: Current language = United State
D: MainAct: Observable Language => China 
D: MainAct: Selected China...
D: BaseAct: changeLocale() ================> zh_CN
D: MainAct: 更新版面, updateText().
D: MainAct: Language swap: => 英语, 向大家问好!, Git 后备管理 

没有重复,加速成功。


☕12. Espresso 自动测错

👶: 完了,下班啦!
👴: 喂,还没完呢,Espresso 呢?
👶: 下班还喝咖啡呀?
👴: 测错呢。

《 Espresso 记录 》

record
add
👴: 你啊,点一个加一个。我先走了。
👶: OK,一个一个又一个。不多,才六个。OK。
save

👽:跑起来试试,没结果,中风了?头不在啊,上网查啊,没有没有…
直接开谷歌的文件,有了。

《 Androidx Gradle Test 修改 》

Androidx 的 Test 有点不一样,加上 Kotlin , Gradle 的资料库要加 ktx。所以要改 module.gradle

android {
    compileSdkVersion 30

    defaultConfig {
        ...
        //testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        //要用 androidx。上面那个根本不会跑。谷歌秀逗了,下班了还要自己弄。
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {...}
    compileOptions { ...}
    kotlinOptions {...}
    buildFeatures {...}
    
    // 我的能跑。
    testOptions {
        animationsDisabled = true // Espresso 要求的
        unitTests {
            includeAndroidResources = true
        }
    }
    lintOptions {
        abortOnError false
    }

    useLibrary 'android.test.runner'
    useLibrary 'android.test.base'
    useLibrary 'android.test.mock'
    packagingOptions {
        exclude 'META-INF/DEPENDENCIES'
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/LICENSE.txt'
        exclude 'META-INF/license.txt'
        exclude 'META-INF/NOTICE'
        exclude 'META-INF/NOTICE.txt'
        exclude 'META-INF/notice.txt'
        exclude 'META-INF/AL2.0'
        exclude 'META-INF/LGPL2.1'
        exclude("META-INF/*.kotlin_module")
    }
}
dependencies {
    // test
    testImplementation 'junit:junit:4.13'
    // arch
    def archcore_version = '2.1.0'
    androidTestImplementation "androidx.arch.core:core-testing:$archcore_version"
    // AndroidJUnitRunner and JUnit Rules
    def test_version = '1.3.0'
    androidTestImplementation "androidx.test:runner:$test_version"
    androidTestImplementation "androidx.test:rules:$test_version"
    androidTestImplementation "androidx.test:core:$test_version"
    androidTestImplementation "androidx.test:core-ktx:$test_version" // Kotlin 加的
    // Assertions
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.2' // Kotlin 加的
    androidTestImplementation 'androidx.test.ext:truth:1.3.0'
    androidTestImplementation 'com.google.truth:truth:1.0'
    
    // Espresso dependencies
    def espresso_version = "3.3.0"
    androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version"
    androidTestImplementation "androidx.test.espresso:espresso-intents:$espresso_version"
    androidTestImplementation "androidx.test.espresso:espresso-accessibility:$espresso_version"
    androidTestImplementation "androidx.test.espresso:espresso-web:$espresso_version"
    androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$espresso_version"
    androidTestImplementation "androidx.test.espresso:espresso-idling-resource:$espresso_version"
...
}

喔,曹,一大堆咧。

《 修改 Espresso 记录 》

开头:

@RunWith(AndroidJUnit4::class) 
@LargeTest
class MainActivityTest {

    @get:Rule
    var mActivityTestRule: ActivityScenarioRule<MainActivity>
            = ActivityScenarioRule(MainActivity::class.java)

把记录的码分类:
测中文的:

    fun testChineseText() {
        val textView = onView(
            allOf(withId(R.id.titleTV), withText("向大家问好!"),
                withParent(withParent(withId(android.R.id.content))),
                isDisplayed()))
        textView.check(matches(withText("向大家问好!")))

        val textView2 = onView(
            allOf(withId(R.id.changeTV), withText("=> 英语"),
                withParent(allOf(withId(R.id.localeFab),
                    withParent(IsInstanceOf.instanceOf(android.view.ViewGroup::class.java)))),
                isDisplayed()))
        textView2.check(matches(withText("=> 英语")))

        val textView3 = onView(
            allOf(
                withText("Git 后备管理"),
                withParent(
                    allOf(
                        withId(R.id.action_bar),
                        withParent(withId(R.id.action_bar_container))
                    )
                ),
                isDisplayed()
            )
        )
        textView3.check(matches(withText("Git 后备管理")))
    }

测英文的:

    fun testEnglishText() {
        val textView4 = onView(
            allOf(withId(R.id.titleTV), withText("Hello World!"),
                withParent(withParent(withId(android.R.id.content))),
                isDisplayed()))
        textView4.check(matches(withText("Hello World!")))

        val textView5 = onView(
            allOf(withId(R.id.changeTV), withText("=> Chinese"),
                withParent(allOf(withId(R.id.localeFab),
                    withParent(IsInstanceOf.instanceOf(android.view.ViewGroup::class.java)))),
                isDisplayed()))
        textView5.check(matches(withText("=> Chinese")))

        val textView6 = onView(
            allOf(
                withText("Git Practice"),
                withParent(
                    allOf(
                        withId(R.id.action_bar),
                        withParent(withId(R.id.action_bar_container))
                    )
                ),
                isDisplayed()
            )
        )
        textView6.check(matches(withText("Git Practice")))
    }

浮动按钮:

    fun fabClick() {
        val coordinatorLayout = onView(
            allOf(withId(R.id.localeFab),
                childAtPosition(
                    childAtPosition(
                        withId(android.R.id.content),
                        0),
                    1),
                isDisplayed()))
        coordinatorLayout.perform(click())
    }

主控部分浓缩成几行,我跑它十次。

    @Test
    fun mainActivityTest() {
        val scenarioRule = mActivityTestRule.scenario

        for (i in 1..10) {
            testChineseText()
            fabClick()
            testEnglishText()
            fabClick()
        }
        
        scenarioRule.close()
    }

test 1
超速啊!
Ctrl+K 上传:
e test1

《 测试 Resume 回来的结果 》

有时候,用家会离开App,又回去用这个App,所以要测 resume 有没有变味,在 scenario 内用 recreate()。

    @Test
    fun mainActivityTest2() {
        val scenarioRule = mActivityTestRule.scenario

        testChineseText()
        fabClick()
        testEnglishText()
        fabClick()

        scenarioRule.recreate()
        testEnglishText()

        scenarioRule.close()
    }

test 2
失败,应该是英文的才对。我在 MainActivity 加个 @TestOnly 测试 专用的方程:

    @TestOnly
    fun getLocale(): String? {
        return mainVM.currentLocale.value
    }

接着就可以提取 locale 测试。用 for…loop,省事,中英连着测。

    @Test
    fun mainActivityTest2() {
        val scenarioRule = mActivityTestRule.scenario
        
        for (i in 1..2) {
            var mLocale = ""
            scenarioRule.onActivity { activity ->
                mLocale = activity.getLocale()!!
                Thread.sleep(2000)
            }
            lgd("Test2: locale = $mLocale")
            testLocale(mLocale!!)
            scenarioRule.recreate()
            lgd("Test2: Resume locale = $mLocale")
            testLocale(mLocale!!)

            fabClick()
        }
        scenarioRule.close()
    }

    fun testLocale(mLocale: String) {
        when (mLocale) {
            CHINA ->  testChineseText()
            US -> testEnglishText() 
        }
    }

跑跑看,太快了,加两秒减速。
test 2
😵:终于测完了,还有吗?
Ctrl+K,上传,收工。

🎁 13. 英文连接

这个是 英文简化版
帮帮忙啦,在那里拍拍手👏。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值