【Android Jetpack实战追踪】从一个简单的登录页开始

在Android开发的设计模式上,大体上经历了MVC、MVP以及如今“甚嚣尘上”的MVVM,而Jetpack的横空出世无疑给MVVM添了一把柴火。

------ 老朽

那么对于一个初学者甚至刚听过这个名词的开发者,该如何入门呢?接下来就让老朽带领。。噢不。。是跟大家一起去深入Jetpack单词的拼写,J-E-T-P-A-C-K,来read after me: 借特派克。

好了,废话不多说,进入正题。

在进入正题前,有必要把我的开发环境列一下:
win10 + Android Studio 4.2 Beta5 + sdk 30 + gradle 6.7.1 + kotlin 1.4.31

今天这篇文章准备刚以下几点:

  • 1、ViewModel简单使用
  • 2、数据绑定
  • 3、双向数据绑定

1、ViewModel简单使用

首先我们引入ViewModel,ViewModel位于androidx.lifecycle包中。
不知道是不是Android Studio或androidx版本原因,我这里建完项目后自动引入lifecycle包,但是查看gradle也没有这个包。
无妨,如果没有的话,大家按照官网提示依赖就可以了。

    def lifecycle_version = "2.3.0"
    def arch_version = "2.1.0"

    // ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$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"

环境准备好了,我们画个登录页先,既然不是讲UI,我们就用IDE简单地拖一个界面:一个用户名输入框,一个密码输入框,一个登录按钮,很快就好了。
在这里插入图片描述
功能有两个:1、用户名自动填充,如果之前输入过的话;2、登录并根据返回结果显示对应信息。
画完了页面,我们定义一个ViewModel,其UML类图如下:
在这里插入图片描述
简单地写点逻辑。、

class LoginViewModel: ViewModel() {

    companion object {
        private const val TAG = "LoginViewModel"
    }

    /**
     * Simply define a string field to express Model.
     */
    var name = MutableLiveData<String>()

    /**
     * For caller to observe the login result.
     */
    val loginResult = MutableLiveData<Boolean>()

    fun loadUser() {
        // Just set a value directly, it may a complicated process in fact, that is, retrieve data from DB or the backend.
        name.value = "Jetpack"
    }

    fun login(name: String, password: String) {
        Log.d(TAG, "login() name = $name password = $password")
        // Simulate Login Executing..., return a random result.
        loginResult.value = Random.nextBoolean()
    }
}

VM写好了,怎么去使用呢?
我们再在Activity里注册观察者。第一个观察者来观察用户名的变化 ,完成第1个功能;第二个观察者来观察登录的结果,完成第二个功能。
代码很简单:

/**
     * ViewModel to provide data for Views.
     */
    private lateinit var mLoginViewModel: LoginViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        // instantiate ViewModel with its no-args constructor.
        mLoginViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(LoginViewModel::class.java)
        val nameView = findViewById<TextView>(R.id.name_et)
        val passwordView = findViewById<TextView>(R.id.password_et)
        // Observe name changes and display the changed value to View.
        mLoginViewModel.name.observe(this, { name ->
            nameView?.text = name
        })
        // Load user for retrieving the latest changes which will change the name field.
        mLoginViewModel.loadUser()
        findViewById<View>(R.id.login_btn)?.setOnClickListener {
            // Simple checking.
            if(TextUtils.isEmpty(nameView.text) || TextUtils.isEmpty(passwordView.text)) {
                Toast.makeText(this, "Invalid name or password(name = ${nameView.text}, password = ${passwordView.text}", Toast.LENGTH_LONG).show()
                return@setOnClickListener
            }
            // Do Login Executing.
            mLoginViewModel.login(nameView.text.toString(), passwordView.text.toString())
        }
        // Observe the login result and show corresponding message.
        mLoginViewModel.loginResult.observe(this, { success ->
            if(success) {
                Toast.makeText(applicationContext, "login success", Toast.LENGTH_LONG).show()
            } else {
                Toast.makeText(applicationContext, "login failed", Toast.LENGTH_LONG).show()
            }
        })
    }

逻辑很好理解,就是通常说的观察者模式,也懒得啰嗦了,一张图(别在乎两个小圆点,就是表示一对眼睛在观察):
在这里插入图片描述
我们来跑一下……

在这里插入图片描述
这便是一个简单的MVVM了,M就是LoginViewModel里的name或loginResult,这里只是图方便,写在VM里面了而已,V就是UI了,VM便是LoginViewModel。

2、数据绑定

LoginActivity:我TM算什么呢?
是啊,这到底是MVVM还是MVCVM啊。就是说你既然用了Activity却不给个名份,换谁心里也难受不是。

那我们就想了,能不能把Activity再简化一下,你就加载个资源和实例化一些对象,至于什么观察、什么绑定赋值啥的你不用管。

于是数据绑定就呼之欲出了……

其实databinding并不是什么新鲜事,只不过之前都是在MVC中把View与Model进行静态绑定,为什么说是静态呢,因为绑定后就完事了,Model值再怎么改变,View是无法感知的,只能苦巴巴地再次调用executePendingBindings()。

那动态的又是怎样的呢?其实就是利用观察者模式,让View盯着Model的变化,一旦改变,View就实时显示更新的数据,那么这类Model我们称为具有生命周期感知能力的对象,英文叫做Lifecycle-aware。

那么想实现动态绑定,安卓中提供了两种方法。

第一种是Android Studio3.1之前的做法,就是把ObservableField把原来的类型包起来,下面我们分别看下这两种方法。
当然了,我们第一步还是在项目中开启databinding,具体做就是修改app的gradle文件。

    dataBinding {
        enabled=true
    }

如果使用kotlin的话,gradle的plugins加上kapt在编译期生成必要的类文件。

id 'kotlin-kapt'//以前好像是plugin XXX,具体方法以gradle版本为准

接下来我们修改布局文件并引入ViewModel,在用户名处绑定自动填充的用户名。

<?xml version="1.0" encoding="utf-8"?>
<layout>

    <data>
        <variable
            name="viewModel"
            type="com.codersth.jetpackpractice.viewmodel.LoginViewModel" />
    </data>
    <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:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".view.ui.LoginActivity">

        <Button
            android:id="@+id/login_btn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginLeft="16dp"
            android:layout_marginEnd="16dp"
            android:layout_marginRight="16dp"
            android:layout_marginBottom="180dp"
            android:padding="16dp"
            android:text="@string/login"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"/>

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/textInputLayout"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginLeft="16dp"
            android:layout_marginTop="180dp"
            android:layout_marginEnd="16dp"
            android:layout_marginRight="16dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/name_et"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ellipsize="end"
                android:hint="@string/please_input_name"
                android:maxLength="16"
                android:singleLine="true"
                android:text="@{viewModel.name}"/>
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginLeft="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="16dp"
            android:layout_marginRight="16dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textInputLayout">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/password_et"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ellipsize="end"
                android:hint="@string/please_input_password"
                android:inputType="textPassword"
                android:maxLength="16"
                android:singleLine="true"/>
        </com.google.android.material.textfield.TextInputLayout>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

改完后ide就自动生成了ActivityLoginBinding(没有就手动make一下)。
然后我们把上面的LoginViewModel简单地改下:

    /**
     * Simply define a string field to express Model.
     */
    var name = ObservableField<String>()
    fun loadUser() {
        // Just set a value directly, it may a complicated process in fact, that is, retrieve data from DB or the backend.
        name.set("Jetpack")
    }

最后,在LoginActivity中完成View与ViewModel的数据绑定。

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Retrieve layout binding.
        val binding: ActivityLoginBinding = DataBindingUtil.setContentView(this, R.layout.activity_login)
        // Bind binding's lifecycle to the current component.
        binding.lifecycleOwner = this
        // instantiate ViewModel with its no-args constructor.
        val loginViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(LoginViewModel::class.java)
        binding.viewModel = loginViewModel
        // Load user for retrieving the latest changes which will change the name field.
        Handler(Looper.getMainLooper()).postDelayed(Runnable {
            loginViewModel.loadUser()
        }, DURATION_SIMULATE_REQUEST)
    }

可以看到,当我们在若干秒后调用loadUser并改变name字段后,用户名被重新赋值。
在这里插入图片描述
至于其它数据类型、集合、对象等,都是一个道理,大家可以去官网逛逛.

第二个绑定方法就是利用LiveData,我们把上面LoginViewModel代码稍微改变如下:

    /**
     * For caller to observe the login result.
     */
    val loginResult = MutableLiveData<Boolean>()

    fun loadUser() {
        // Just set a value directly, it may a complicated process in fact, that is, retrieve data from DB or the backend.
        name.value = "Jetpack"
    }

效果是一样的,Android Studio3.1后基本上都在大量使用LiveData。

3、双向数据绑定

MD,码字是真累,你们看到这里的一定要记得给老朽点个赞。

虽然说数据绑定很好用,但大家注意到了没,上面这种绑定虽然是动态的,但却是单向的,也就是说M的修改可以影响V,但反过来却不行。

我们都知道在layout中不仅可以绑定属性,还可绑定方法,我们不妨先把ViewModel中登录方法改下:

    /**
     * Binding for layout and called by login clicked.
     */
    fun login() {

    }

然后在登录按钮中绑定下。

<Button
            android:id="@+id/login_btn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginLeft="16dp"
            android:layout_marginEnd="16dp"
            android:layout_marginRight="16dp"
            android:layout_marginBottom="180dp"
            android:padding="16dp"
            android:text="@string/login"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:onClick="@{() -> viewModel.login()}"/>

但是……用户名和密码怎么传进去呢?
如果说我在LoginViewModel定义两个字段,然后分别绑定layout中的用户名和密码,再使用双向绑定,这样当用户输入后,LoginViewModel中的两个字段总能取到最新值,拿出来做为参数做逻辑处理不就完事了?
在这里插入图片描述

然而,根据官网的说法,我大ViewModel要实现Observable接口,为了省事,我就把官网demo中的ObservableViewModel偷来了。
这样LoginViewModel就变成:
在这里插入图片描述
其中新增两个方法getName和getPassword并加注解@Bindable,表示V通过这个方法取值。

 /**
     * Annotation as [Bindable] for two-way binding with view.
     */
    @Bindable
    fun getName(): String {
        return nameData
    }

    @Bindable
    fun getPassword(): String {
        return passwordData
    }

对于set方法则无要求,需要注意的是判断值确实有更新才通知V,因为你不判断的话,就进入“我更新你更新我更新你……”的无限纠缠之中。

    fun setName(newValue: String) {
        if(nameData != newValue) {
            nameData = newValue
            notifyPropertyChanged(BR.name)
        }
    }

其中notifyPropertyChanged就是通知V更新值,BR.name中的"name"需要与getName()方法中的"name"保持一致(别忘了make一下)。

经过这样一番折腾,login方法中的逻辑就可以全部写到VM中了。

/**
     * Binding for layout and called by login clicked.
     */
    fun login() {
        Log.d(TAG, "login name = $nameData and password = $passwordData")
        // Simple checking.
        if(TextUtils.isEmpty(nameData) || TextUtils.isEmpty(passwordData)) {
//            Toast.makeText(this, "Invalid name or password(name = ${nameView.text}, password = ${passwordView.text}", Toast.LENGTH_LONG).show()
            Log.d(TAG, "Invalid name or password(name = ${nameData}, password = ${passwordData}")
            return
        }
        // Do Login Executing.
        loginResult.value = Random.nextBoolean()
    }

在Activity中我们几乎只做了两件事:1)初始化;2)处理结果,某些场景这个都可以在VM中做掉。

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Retrieve layout binding.
        val binding: ActivityLoginBinding = DataBindingUtil.setContentView(this, R.layout.activity_login)
        // Bind binding's lifecycle to the current component.
        binding.lifecycleOwner = this
        // instantiate ViewModel with its no-args constructor.
        val loginViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(LoginViewModel::class.java)
        binding.viewModel = loginViewModel
        // Load user for retrieving the latest changes which will change the name field.
        Handler(Looper.getMainLooper()).postDelayed(Runnable {
            loginViewModel.loadUser()
        }, DURATION_SIMULATE_REQUEST)
        // Observe the login result and show corresponding message.
        mLoginViewModel.loginResult.observe(this, { success ->
            if(success) {
                Toast.makeText(applicationContext, "login success", Toast.LENGTH_LONG).show()
            } else {
                Toast.makeText(applicationContext, "login failed", Toast.LENGTH_LONG).show()
            }
        })
        mLoginViewModel = loginViewModel
    }

这样一个简单的MVVM的雏形便出来了!

好了,今天就学习这么多,有时间再继续追踪~

以上案例请分别参见以下commit:
在这里插入图片描述
GIT:https://github.com/codersth/JetpackPractice.git

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Meta章磊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值