UI - 登录注册
下面我们将进行Android UI的学习,这个可以讲的很简单,也可以讲的很深,我将不会从简单的一个一个控件开始讲述,而是从功能去讲述UI,本篇文章以常规项目的模式为主,也就是有XML,当然我也会写一篇Compose来编写功能的文章,好了,下面我们进入正题。
最终效果图如下所示:

这里你看到中间有黑屏的地方不要慌,那不是报错,那是因为录屏的时候当输入密码时是不可以录制的,所以就黑屏了,等输入完成之后就可以了,你在实际运行的时候不会这样的。
一、创建项目
下面创建项目,记得要选择Empty Views Activity
。

就命名为Android-UI
,后续对于UI的讲述都在这个项目下,我也会提供源码给你,点击Finish
完成项目创建。
二、创建页面
一般情况下,我们做这种页面开发,一个单独的页面都是创建一个Activity的,它是Android中的四大组件之一,很重要,下面我们在项目中创建一个登录页面和一个注册页面。
鼠标右键点击这个包名目录 → New → Activity → Empty Views Activity
。

通过这个图我们就知道创建Activity的入口,这个我只说一次,你创建几次就知道怎么找到了,无须多言。

这里我们创建一个LoginActivity作为登录页面,这里注意一点就是,我们通过上述方式创建Activity时,进入这个窗口时,Generate Layout File
选项会默认勾选上,它的意思是生成对应的布局XML文件,文件名称就是activity_login,格式就是.xml。
而下面这个Launcher Activity
是默认不勾选的,我勾选上的,因为我们创建项目的时候有一个默认的MainActivity,它是启动页,而现在我们需要将登录作为启动页面,所以我这里勾选上,那么下一次我们启动App的时候就会直接打开登录页面,而不是主页面,当然这个配置我们也可以在AndroidManifest.xml中进行改动。其他的就没有什么好解释的了,点击Finish完成登录页面的创建。

我们就可以在这里看到一个LoginActivity,然后我们再用同样的方式创建一个注册页面,名称是RegisterActivity,这里就不用勾选那个Launcher Activity
选项了。

因为启动页面只有一个,不需要那么多启动页面,注册页面我们可以从登录页面跳转过去。你可能会好奇,如果注册页面也设置为启动页面会怎么样。我们先完成页面的创建,然后再说这个问题。
三、启动顺序
这里就涉及到Activity相关的知识点,那么我们就说一下,这里我们目前有两个去启动的Activity,分别是MainActivity
和LoginActivity
,我们打开AndroidManifest.xml
看一下。

在运行之前,你可以先猜一下,启动的是那个Activity,然后我们再运行。运行的结果是,启动了LoginActivity,而不是MainActivity。

那我们再把MainActivity的配置放到前面去,在运行一下就是启动的MainActivity了,由此我们知道了一个点,那就是当我们有多个Activity配置了启动页面后,谁在前面就启动谁,这个就和xml文件的读取机制有关系,从上往下进行读取的,如果说我们的AndroidManifest.xml
中只有一个是启动页面,那么不管它在什么位置都会启动它。
还有一个点,那就是activity标签中的exported
属性,在Android 12及以上的版本中增加了这个属性,如果你在对应的手机上运行而不配置的话就会报错,一般启动页面的配置是true,其他的是false。这个属性是一个安全属性,控制其他应用能否直接启动该Activity。
- false:仅允许同一应用(或相同用户ID的应用)启动此Activity。
- true:允许其他应用(如浏览器、其他APP)通过Intent调用它。

最后我们再改动一下,这里我就把LoginActivity作为启动页,并且去掉了MainActivity的相关配置。
四、布局
下面我们打开activity_login.xml,先看看里面的代码:
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LoginActivity">
</androidx.constraintlayout.widget.ConstraintLayout>
ConstraintLayout是一个约束布局,允许我们通过拖动锚点的方式对页面的控件进行布局,是现在主推的方式,当然还有其他布局方式、线性布局、相对布局、帧布局、网格布局、协调布局,你没有必要去学习所有布局,而是要用最合适的方式去布局。
那么我们如何选择布局?
- 简单列表/表单 → LinearLayout
- 复杂动态界面 → ConstraintLayout(首选)
- 重叠或单控件 → FrameLayout
- Material Design动画 → CoordinatorLayout
那对于我们的登录页面来说我们用什么布局好一些呢?这里就要分析这个页面需要哪些控件了,账号、密码输入框、登录按钮、注册入口。
我们先设计的简单一些,上述的控件我们就可以用线性布局去实现,activity_login.xml
代码如下所示:
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".LoginActivity">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:contentDescription="LOGO"
android:src="@mipmap/ic_launcher" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="32dp">
<EditText
android:id="@+id/et_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="username"
android:hint="账号" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp">
<EditText
android:id="@+id/et_pwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="pwd"
android:hint="密码"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="32dp"
android:insetTop="0dp"
android:insetBottom="0dp"
android:text="登录"
app:cornerRadius="16dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_register"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:insetTop="0dp"
android:insetBottom="0dp"
android:text="注册"
app:cornerRadius="16dp" />
</LinearLayout>
运行后的效果图如下所示:

上述代码, 似乎没有什么能讲的,如果你觉得有需要的话,可以评论或者留言,下面我们再编写注册页面的布局代码activity_register.xml
,如下所示:
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".LoginActivity">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:contentDescription="LOGO"
android:src="@mipmap/ic_launcher" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="32dp">
<EditText
android:id="@+id/et_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="username"
android:hint="账号" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp">
<EditText
android:id="@+id/et_pwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="pwd"
android:hint="密码"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp">
<EditText
android:id="@+id/et_pwd_again"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="username"
android:hint="再次输入密码"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_register"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="32dp"
android:insetTop="0dp"
android:insetBottom="0dp"
android:text="注册"
app:cornerRadius="16dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_login"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:insetTop="0dp"
android:insetBottom="0dp"
android:text="已有账号,去登录"
app:cornerRadius="16dp" />
</LinearLayout>
注册页面和登录页面也没差太多,上面有很多的共同点,不过我们还是简单说一下,这里输入框我们通过TextInputLayout(Material Design)
增强效果,用它包裹EditText
,输入时提示文字会浮动到输入框上方。上面我们需要用到的控件都给了一个id,我们在代码中需要通过id去找到对应的控件。
最终我们需要进入到主页面MainActivity,我们将账号和密码显示在这个主页面上,修改布局代码activity_main.xml
中的TextView,添加一个id,如下图所示:

五、业务逻辑
这里的业务逻辑比较简单,因为我们设计的就比较简单,当我们点击登录或者注册按钮时需要检查当前输入框是否有内容,有的话就可以执行登录或者注册的方法,这里我们就不对输入框的内容进行规则检查了,先做的简单一些,注册成功之后就通过本地持久化技术保存起来,在登录页面登录时拿出数据进行对比,一致则进入主页面,是不是很简单呢?
下面我们进入逻辑编写的环节。
1. 本地数据持久化
这里我们使用到SharedPreferences
来做数据持久化,它是轻量级的,并不是数据库,数据库后面再说。
我们在com.example.android_ui
包下创建一个utils
包,参考下图方式,只说明一次
然后在utils包下创建一个SPUtils
类,注意我们需要创建的是Kotlin文件,参考下图方式:

然后编写SPUtils
类的代码,如下所示:
kotlin
package com.example.android_ui.utils
import android.content.Context
import android.content.SharedPreferences
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class SPUtils private constructor(context: Context, name: String = DEFAULT_SP_NAME) {
companion object {
const val DEFAULT_SP_NAME = "sp_config"
private var instance: SPUtils? = null
@Synchronized
fun initialize(context: Context, name: String = DEFAULT_SP_NAME): SPUtils {
return instance ?: SPUtils(context.applicationContext, name).also { instance = it }
}
fun getInstance(): SPUtils {
return instance ?: throw IllegalStateException("SPUtils not initialized. Call initialize() first.")
}
}
private val sharedPref: SharedPreferences by lazy {
context.getSharedPreferences(name, Context.MODE_PRIVATE)
}
// 基本操作
fun putString(key: String, value: String) = sharedPref.edit().putString(key, value).apply()
fun getString(key: String, default: String = "") = sharedPref.getString(key, default) ?: default
fun putInt(key: String, value: Int) = sharedPref.edit().putInt(key, value).apply()
fun getInt(key: String, default: Int = 0) = sharedPref.getInt(key, default)
fun putLong(key: String, value: Long) = sharedPref.edit().putLong(key, value).apply()
fun getLong(key: String, default: Long = 0L) = sharedPref.getLong(key, default)
fun putFloat(key: String, value: Float) = sharedPref.edit().putFloat(key, value).apply()
fun getFloat(key: String, default: Float = 0f) = sharedPref.getFloat(key, default)
fun putBoolean(key: String, value: Boolean) = sharedPref.edit().putBoolean(key, value).apply()
fun getBoolean(key: String, default: Boolean = false) = sharedPref.getBoolean(key, default)
fun putStringSet(key: String, value: Set<String>) = sharedPref.edit().putStringSet(key, value).apply()
fun getStringSet(key: String, default: Set<String> = emptySet()) = sharedPref.getStringSet(key, default) ?: default
// 其他操作
fun contains(key: String) = sharedPref.contains(key)
fun remove(key: String) = sharedPref.edit().remove(key).apply()
fun clear() = sharedPref.edit().clear().apply()
fun getAll() = sharedPref.all
// 使用委托属性简化访问
fun stringPref(key: String, default: String = "") =
object : ReadWriteProperty<Any, String> {
override fun getValue(thisRef: Any, property: KProperty<*>) = getString(key, default)
override fun setValue(thisRef: Any, property: KProperty<*>, value: String) = putString(key, value)
}
fun intPref(key: String, default: Int = 0) =
object : ReadWriteProperty<Any, Int> {
override fun getValue(thisRef: Any, property: KProperty<*>) = getInt(key, default)
override fun setValue(thisRef: Any, property: KProperty<*>, value: Int) = putInt(key, value)
}
fun booleanPref(key: String, default: Boolean = false) =
object : ReadWriteProperty<Any, Boolean> {
override fun getValue(thisRef: Any, property: KProperty<*>) = getBoolean(key, default)
override fun setValue(thisRef: Any, property: KProperty<*>, value: Boolean) = putBoolean(key, value)
}
}
// 扩展函数简化使用
fun Context.sp(name: String = SPUtils.DEFAULT_SP_NAME) = SPUtils.initialize(this, name)
这里就是一个简单的工具类,SP就是键值对的方式对数据进行存取,我们需要对这个工具类进行一个初始化。
2. 初始化
工具类方法中有一个initialize()
方法,我们在使用之前要先初始化,否则会报错,初始化我们可以在程序执行的时候进行初始化,我们在com.example.android_ui
下创建一个MyApplication
类,里面的代码如下:
kotlin
package com.example.android_ui
import android.app.Application
import com.example.android_ui.utils.SPUtils
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
SPUtils.initialize(this)
}
}
这里需要说一下它的作用,这里继承 Android 的 Application 类,用于全局应用级别的初始化,可以在此类中初始化全局工具(如数据库、网络库、SharedPreferences 工具等),可以维护应用级别的状态或变量,比在 Activity 中初始化更高效,因为 Application 只会创建一次。
3. AndroidManifest.xml配置
在上面我们写好了这个类,为了使这个类生效,我们需要在AndroidManifest.xml
中配置配置它,很简单,配置如下图所示:

虽然很简单,但是很容易会被忘记,所以你要记得呀。
4. 常量类
前面我们说过SP是针对于键值对的形式,也就是Key-Value的形式,你可以理解钥匙和锁的关系,一对一的,那么我们的Key一般是不变的,所以我们可以用一个常量类去存放这些Key,在utils包下新建一个EasyConstants类,代码如下所示:
kotlin
object EasyConstants {
const val KEY_USERNAME = "username"
const val KEY_PASSWORD = "password"
}
这里我目前只写了两个Key,账号和密码,后面再持续增加,这里就告一段落,下面我们就要进入LoginActivity
中去编写代码了。
5. findViewById
因为是常规项目,所以我们有XML,xml中的代码我们之前都已经写好了,那么下面我们要做的就是在代码文件中去操作xml中的控件去实现我们的业务逻辑。
在此之前我们还是需要了解一下findViewById
的作用:
- findViewById(int id) 是一个用于通过布局文件中的ID查找视图(View)对象的方法,定义在 Activity 和 View 类中。
- 连接布局与代码的桥梁,将XML布局文件中定义的UI组件()与Java/Kotlin代码关联,使开发者能够在代码中操作界面元素
- 视图控制的基础,获取到View对象后,可以:
- 修改属性(文字、颜色、可见性等)
- 设置事件监听(点击、长按等)
- 动态改变布局
针对这个我们可以使用传统的方式和现代改进方案ViewBinding,我们先使用传统的方式去写登录页面,LoginActivity代码如下所示:
kotlin
package com.example.android_ui
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.android_ui.utils.EasyConstants
import com.example.android_ui.utils.SPUtils
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_login)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
initView()
}
private fun initView() {
// 获取控件
val etUserName = findViewById<EditText>(R.id.et_username)
val etPassword = findViewById<EditText>(R.id.et_pwd)
// 登录
findViewById<Button>(R.id.btn_login).setOnClickListener {
val username = etUserName.text.toString().trim()
if (username.isEmpty()) {
showMsg("请输入用户名")
return@setOnClickListener
}
val password = etPassword.text.toString().trim()
if (password.isEmpty()) {
showMsg("请输入密码")
return@setOnClickListener
}
// 校验账号密码
if (username == SPUtils.getInstance().getString(EasyConstants.KEY_USERNAME) &&
password == SPUtils.getInstance().getString(EasyConstants.KEY_PASSWORD)) {
showMsg("登录成功")
// 进入主界面
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
// 关闭当前界面
finish()
}
}
// 注册
findViewById<Button>(R.id.btn_register).setOnClickListener {
// 清空输入框
etUserName.text.clear()
etPassword.text.clear()
// 跳转到注册界面
startActivity(Intent(this, RegisterActivity::class.java))
}
}
private fun showMsg(msg: String) {
// 弹出吐司
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
}
在initView方法中,我们简单说明一下:
-
基本结构:
- 继承自
AppCompatActivity
,是一个标准的 Android Activity - 使用
enableEdgeToEdge()
实现边缘到边缘的全面屏显示 - 通过
setContentView
设置布局文件为activity_login
- 继承自
-
视图初始化:
initView()
方法中获取了两个 EditText(用户名和密码输入框)和两个 Button(登录和注册按钮)- 使用
findViewById
获取视图控件
-
登录逻辑:
- 点击登录按钮时,获取输入的用户名和密码
- 检查输入是否为空,为空则显示提示
- 验证用户名密码是否与 SPUtils 中存储的匹配(从 SharedPreferences 读取)
- 验证成功则跳转到 MainActivity 并关闭当前界面
-
注册逻辑:
- 点击注册按钮时清空输入框
- 跳转到 RegisterActivity
-
辅助方法:
showMsg()
显示 Toast 提示信息
-
其他特性:
- 使用 ViewCompat 处理系统窗口插入(如状态栏、导航栏)
- 使用 trim() 处理输入字符串前后的空格
- 使用 return@setOnClickListener 从 lambda 表达式返回
下面我们使用ViewBinding去写RegisterActivity的代码。
6. ViewBinding
ViewBinding的使用,我们需要现在当前模块的build.gradle文件中去配置打开,如下图所示:
修改build.gradle文件之后要记得点击Sync Now
,否则不会生效,同步的时候AS底部栏会有一个加载状态条,当这个条消失时表示同步完成,我们配置就完成了,就可以正式去使用ViewBinding了,这个后面我们就默认都是打开的了,也不会再重复说明了。
下面进入到注册页面,RegisterActivity
代码如下所示:
kotlin
package com.example.android_ui
import android.os.Bundle
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.android_ui.databinding.ActivityRegisterBinding
import com.example.android_ui.utils.EasyConstants
import com.example.android_ui.utils.SPUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class RegisterActivity : AppCompatActivity() {
private lateinit var binding: ActivityRegisterBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityRegisterBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
initView()
}
private fun initView() {
// 注册
binding.btnRegister.setOnClickListener {
val username = binding.etUsername.text.toString().trim()
if (username.isEmpty()) {
showMsg("请输入用户名")
return@setOnClickListener
}
val password = binding.etPwd.text.toString().trim()
if (password.isEmpty()) {
showMsg("请输入密码")
return@setOnClickListener
}
val passwordAgain = binding.etPwdAgain.text.toString().trim()
if (passwordAgain.isEmpty()) {
showMsg("请再次输入密码")
return@setOnClickListener
}
// 检查两次密码是否一致
if (password != passwordAgain) {
showMsg("两次密码不一致")
return@setOnClickListener
}
// 注册账号,保存账号到SP,进行本地数据持久化,如果清除缓存则会消失
SPUtils.getInstance().putString(EasyConstants.KEY_USERNAME, username)
SPUtils.getInstance().putString(EasyConstants.KEY_PASSWORD, password)
showMsg("注册成功,稍后进入登录页面进行登录")
// 一秒后返回登录页面
CoroutineScope(Dispatchers.Main).launch {
delay(1000)
finish()
}
}
// 返回登录页面
binding.btnLogin.setOnClickListener {
finish()
}
}
private fun showMsg(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
}
整体逻辑和登录页面差不都,我们就简单说一下ViewBinding:
ViewBinding 是 Android 官方提供的一种视图绑定技术,它能够:
- 为每个 XML 布局文件自动生成绑定类
- 提供类型安全的视图引用
- 替代传统的 findViewById() 方法
- 避免空指针异常和类型转换错误
针对与xml文件和生成的绑定类名:
XML 文件名 | 生成的绑定类名 |
---|---|
activity_main.xml | ActivityMainBinding |
fragment_home.xml | FragmentHomeBinding |
item_user.xml | ItemUserBinding |
最后我们再写MainActivity中的代码:
kotlin
package com.example.android_ui
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.android_ui.databinding.ActivityMainBinding
import com.example.android_ui.utils.EasyConstants
import com.example.android_ui.utils.SPUtils
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
binding.tvTest.text = "用户名:${SPUtils.getInstance().getString(EasyConstants.KEY_USERNAME)} \n " +
"密码:${SPUtils.getInstance().getString(EasyConstants.KEY_PASSWORD)}"
}
}
这里就是进入主页面之后就根据缓存显示账号和密码,下面运行一下:

六、源码
GitHub源码地址: Android-UI
好的,后会有期~