iT邦幫忙

2021 iThome 鐵人賽

DAY 28
0
Mobile Development

花30天做個Android小專案系列 第 28

Day28 - 儲存帳密及自動登入

今天來做儲存帳密和自動登入的功能。

提醒:今天的內容缺少了加密儲存密碼,是極度危險的功能,這部份預計會放到明天處理。

Layout的部份我一樣使用Chip取代Checkbox,在Login頁面中加入:

<!-- ... -->
<com.google.android.material.chip.ChipGroup
    android:id="@+id/chipGroup"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:alpha="0"
    app:layout_constraintBottom_toTopOf="@id/login"
    app:layout_constraintEnd_toEndOf="@id/pwdLayout"
    app:layout_constraintStart_toStartOf="@id/pwdLayout"
    app:layout_constraintTop_toBottomOf="@id/pwdLayout">

    <com.google.android.material.chip.Chip
        android:id="@+id/saveId"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:checkable="true"
        android:fontFamily="sans-serif"
        android:text="記住ID"
        android:textSize="16sp"
        android:textStyle="bold" />

    <com.google.android.material.chip.Chip
        android:id="@+id/savePwd"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:checkable="true"
        android:fontFamily="sans-serif"
        android:text="記住密碼"
        android:textSize="16sp"
        android:textStyle="bold" />

    <com.google.android.material.chip.Chip
        android:id="@+id/auto_login"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:checkable="true"
        android:fontFamily="sans-serif"
        android:text="自動登入"
        android:textSize="16sp"
        android:textStyle="bold"
        android:visibility="gone" />
</com.google.android.material.chip.ChipGroup>
<!-- ... -->

SharePreferences

儲存的內容會存到SharePreferences中,先做一些前置作業:

const val PREF_NAME = "preferences"
const val PREF_FIELD_ID = "id"
const val PREF_FIELD_PWD = "pwd"
const val PREF_FIELD_AES_KEY = "aes_key"
const val PREF_FIELD_AUTO_LOGIN = "auto_login"

class LoginFragment : Fragment() {
    // ...
    private lateinit var preferences: SharedPreferences
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        preferences = view.context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
    }
}

PREF_NAME是在Day21中已加入的值,其餘是這兩天會用到的。

儲存帳密及是否自動登入

儲存的時機點我是放在登入成功時,以下片段節自Day07

// ...
4 -> { // "【主功能表】"
    withContext(Dispatchers.Main) {
        val editor = preferences.edit()
        if (binding.saveId.isChecked) {
            editor.putString(PREF_FIELD_ID, id)
        }

        if (binding.savePwd.isChecked) {
            editor.putString(PREF_FIELD_PWD, pwd)
        }
        editor.putBoolean(
            PREF_FIELD_AUTO_LOGIN,
            binding.autoLogin.isChecked
        )
        editor.apply()
        (requireActivity() as MainActivity).dismissLoading()
        NavHostFragment.findNavController(this@LoginFragment)
            .navigate(R.id.action_loginFragment_to_searchArticleFragment)
    }
    break
}
// ...

提取帳密和是否自動登入

提取的位置我是放在開始執行進入動畫前:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    // ...
    binding.chipGroup.postDelayed({
        val id = preferences.getString(PREF_FIELD_ID, null)
        val pwd = preferences.getString(PREF_FIELD_PWD, null)
        if (!id.isNullOrBlank()) {
            binding.idInput.setText(id)
            binding.saveId.isChecked = true
        }
        if (!pwd.isNullOrBlank()) {
            binding.pwdInput.setText(pwd)
            binding.savePwd.isChecked = true
        }
        if (binding.saveId.isChecked && binding.savePwd.isChecked) {
            binding.autoLogin.visibility = View.VISIBLE
            binding.autoLogin.isChecked =
                preferences.getBoolean(PREF_FIELD_AUTO_LOGIN, false)
        }

        enterAnimate(binding.chipGroup)
    }, 267)
    // ...
}

autoLogin的檢查條件除了本身是否紀錄有勾選外,還需要多判斷是否有已儲存的帳密,此外autoLogin的按鈕我預設是隱藏的,只有在saveIdsavePwd都被打勾的狀態下才能勾選。

Chip點擊動作

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    // ...
    binding.saveId.setOnCheckedChangeListener { _, isChecked ->
        checkShowAutoLogin()
        if (!isChecked) {
            preferences.edit().remove(PREF_FIELD_ID).apply()
        }
    }
    binding.savePwd.setOnCheckedChangeListener { _, isChecked ->
        checkShowAutoLogin()
        if (!isChecked) {
            preferences.edit().remove(PREF_FIELD_PWD).apply()
        }
    }
    binding.autoLogin.setOnCheckedChangeListener { _, isChecked ->
        preferences.edit().putBoolean(PREF_FIELD_AUTO_LOGIN, isChecked).apply()
    }
    // ...
}

private fun checkShowAutoLogin() {
    if (binding.saveId.isChecked && binding.savePwd.isChecked) {
        binding.autoLogin.visibility = View.VISIBLE
    } else {
        binding.autoLogin.visibility = View.GONE
        binding.autoLogin.isChecked = false
    }
}

如前所述autoLogin只有在saveIdsavePwd都被打勾的狀態下才能勾選,因此這兩個按鈕在點擊時會去呼叫checkShowAutoLogin來判斷目前狀態,其餘內容就是取消勾選時移除目前已儲存的資料。

自動登入

在處理自動登入時,除了autoLogin的勾選狀態外,還需要考慮到登出斷線重連的狀態。在這兩個狀態中我們不該執行自動登入的功能。

為了處理上述兩個狀態,我分別在loginFragment的fragment tag和welcome_to_login的action tag中加入Argument。

fragment tag

<argument
    android:name="canAutoLogin"
    android:defaultValue="false"
    app:argType="boolean"
    app:nullable="false" />

action tag

<argument
    android:name="canAutoLogin"
    android:defaultValue="true"
    app:argType="boolean"
    app:nullable="false" />

可以看到兩者的差異只差在defaultValue,只有在從WelcomeFragment到LoginFragment的action中才會將canAutoLogin設為true

有了這個參數值,就可以做以下判斷:

// ...
if (preferences.getBoolean(PREF_FIELD_AUTO_LOGIN, false)
    && LoginFragmentArgs.fromBundle(requireArguments()).canAutoLogin
) {
    binding.login.performClick()
}
// ...

至此自動登入功能也就完成了。

今日功能畫面

https://i.imgur.com/IeNR2li.gif

最後再次提醒:今天的內容缺少了加密儲存密碼,是極度危險的功能,這部份預計會放到明天處理。


上一篇
Day27 - 登出及連線中斷
下一篇
Day29 - 使用Keystore加密密碼
系列文
花30天做個Android小專案30

尚未有邦友留言

立即登入留言