一,功能介绍

采用Kotlin开发语言,项目中使用了DataBinding,ViewModel,LiveData 基于MVVM的架构思想实现。
主要包含两个模块
1,登录功能根据用户输入的手机号和密码进行校验,如果校验通过,跳转到主界面,否则以toast提示用户
具体需求列表:
手机号和密码输入框中当有内容时,显示清除按钮,点击清除按钮时,清空当前输入框的内容
用户输入的手机号以前三后四位的格式进行分割,当输入的手机号超过11位时,焦点自动切换到密码输入框
密码输入框右侧,增加密码可见性切换功能
登录按钮默认不可点击,根据手机号输入框长度大于11位时,且密码输入框内容超过6位时,登录按钮设置可点击。
点击登陆时,根据用户输入的手机号和密码进行校验,校验失败,提示用户,校验成功,则直接跳转至主页面
2,主界面数据列表的展示,根据网络请求返回的数据进行解析,完成后,赋值给列表进行展示。

二,功能实现

一,创建Base类

1)创建公用的网络返回值

//用户网络请求的状态
sealed class HttpResult<out T:Any>{
    data class Success<out T :Any>(val data :Any?):HttpResult<T>()
    data class Error(val msg:String):HttpResult<Nothing>()
}

2)UI状态

//U状态
object  UiState{
    const val Loading  = 0
    const val Success = 1
    const val Empty = 2
    const val Failed = 3
}

3)创建公用的Activity/Fragment

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

abstract class BaseActivity<T :ViewDataBinding,VM:ViewModel>:AppCompatActivity(){
    protected lateinit var binding:T
    protected lateinit var viewModel:VM
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, getLayoutId())
        initView()
        initViewModel()
        observerData()
        initEvent()
    }

    private fun initViewModel(){
        viewModel =  ViewModelProvider(this,
            ViewModelProvider.AndroidViewModelFactory(application)).get(getVMClass())
        binding.lifecycleOwner = this
    }

    abstract fun getLayoutId():Int

    abstract fun getVMClass():Class<VM>

    open fun observerData(){

    }

    open fun initView(){

    }

    override fun onResume() {
        super.onResume()
        loadData()
    }

    open fun initEvent(){

    }

    open fun loadData(){

    }
}

BaseFragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

abstract class BaseFragment<T : ViewDataBinding,VM: ViewModel>:Fragment() {
    protected lateinit var binding:T
    protected lateinit var viewModel:VM

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate<T>(inflater,getLayoutId(),container,false)
        return binding.root
    }

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

    private fun initViewModel(){
        viewModel =  ViewModelProvider(this,
            ViewModelProvider.AndroidViewModelFactory(requireActivity().application)).get(getVMClass())
        binding.lifecycleOwner = this
    }


    abstract fun getLayoutId():Int

    abstract fun getVMClass():Class<VM>

    open fun observerData(){

    }

    open fun initEvent(){

    }
}
二,登录的实现效果

kotlin 配置ndk打包架构 kotlin+jetpack_android


1,创建登陆的Activity

import android.content.Intent
import android.widget.Toast
import com.gaosi.databindingsimple.R
import com.gaosi.databindingsimple.base.BaseActivity
import com.gaosi.databindingsimple.base.HttpResult
import com.gaosi.databindingsimple.databinding.ActivityLoginBinding
import com.gaosi.databindingsimple.main.MainActivity

class LoginActivity :BaseActivity<ActivityLoginBinding, LoginViewModel>() {

    override fun getLayoutId() = R.layout.activity_login

    override fun getVMClass() = LoginViewModel::class.java

    override fun observerData() {
        super.observerData()
        binding.viewmodel = viewModel
    }

    override fun initEvent() {
        super.initEvent()
        viewModel.loginResult.observe(this){
           when(it){
               is HttpResult.Success ->{
                   jumpListPage()
               }
               is HttpResult.Error ->{
                   showToast(it.msg)
               }
           }
        }
    }

    private fun showToast(msg:String){
        Toast.makeText(this,msg,Toast.LENGTH_SHORT).show()
    }

    private fun jumpListPage(){
        startActivity(Intent(this,MainActivity::class.java))
    }
}

2,对应的xml布局

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="viewmodel"
            type="com.gaosi.databindingsimple.login.LoginViewModel"/>
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <EditText
            android:id="@+id/et_phone"
            android:layout_width="match_parent"
            android:layout_marginTop="120dp"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginStart="16dp"
            android:hint="手机号"
            android:inputType="number"
            android:imeOptions="actionDone"
            android:layout_marginEnd="16dp"
            android:selection="@{viewmodel.phoneIndex}"
            android:text="@{viewmodel.phoneText}"
            addTextChangeListener="@{viewmodel.phoneTextChange}"
            android:layout_height="wrap_content" />

        <ImageView
            android:id="@+id/iv_clear"
            android:layout_width="wrap_content"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="@+id/et_phone"
            android:src="@android:drawable/ic_menu_delete"
            android:layout_marginEnd="16dp"
            android:onClick="@{()->viewmodel.cleanPhone()}"
            controlVisible="@{viewmodel.phoneText.length()}"
            app:layout_constraintBottom_toBottomOf="@+id/et_phone"
            android:layout_height="wrap_content"/>

        <EditText
            android:id="@+id/et_pwd"
            android:layout_width="match_parent"
            android:layout_marginTop="16dp"
            app:layout_constraintTop_toBottomOf="@id/et_phone"
            android:layout_marginStart="16dp"
            android:layout_marginEnd="16dp"
            android:text="@{viewmodel.passwordText}"
            addFocusChangeListener="@{viewmodel.passwordFocus}"
            addTextChangeListener="@{viewmodel.pwdTextChange}"
            android:inputType="@{viewmodel.inputType}"
            android:hint="密码"
            android:layout_height="wrap_content" />

        <ImageView
            android:id="@+id/iv_clearPwd"
            android:layout_width="wrap_content"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="@+id/iv_control"
            app:layout_constraintRight_toLeftOf="@+id/iv_control"
            android:src="@android:drawable/ic_menu_delete"
            android:layout_marginEnd="8dp"
            android:onClick="@{()->viewmodel.cleanPassword()}"
            controlVisible="@{viewmodel.passwordText.length()}"
            android:layout_marginRight="16dp"
            app:layout_constraintBottom_toBottomOf="@+id/iv_control"
            android:layout_height="wrap_content"/>

        <ImageView
            android:id="@+id/iv_control"
            android:layout_width="wrap_content"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="@+id/et_pwd"
            android:src="@drawable/rg_see_pwd"
            android:layout_marginEnd="16dp"
            controlPwd="@{viewmodel.pwdStatus}"
            android:onClick="@{()->viewmodel.controlPwd()}"
            app:layout_constraintBottom_toBottomOf="@+id/et_pwd"
            android:layout_height="wrap_content"/>

        <Button
            android:id="@+id/btn_login"
            android:layout_width="match_parent"
            app:layout_constraintTop_toBottomOf="@+id/et_pwd"
            android:text="login"
            android:background="@drawable/selector_fillet_eighteen_login"
            android:layout_marginStart="16dp"
            android:layout_marginTop="36dp"
            android:onClick="@{()->viewmodel.login()}"
            android:enabled="@{viewmodel.loginBtnStatus}"
            android:layout_marginEnd="16dp"
            android:layout_height="wrap_content" />

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

3,创建登录对应的ViewModel

import android.app.Application
import android.text.Editable
import android.text.InputType
import android.text.TextWatcher
import android.util.Log
import androidx.lifecycle.*
import com.gaosi.databindingsimple.base.HttpResult
import kotlinx.coroutines.launch
import java.lang.Exception

class LoginViewModel(application: Application) :AndroidViewModel(application) {

    val model by lazy {
        LoginRepository()
    }

    private val _loginResult = MutableLiveData<HttpResult<String>>()
    val loginResult :LiveData<HttpResult<String>> = _loginResult
    //文本框类型状态
    private val _inputType =  MutableLiveData(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD)
    val inputType: LiveData<Int> = _inputType

    private val _pwdStatus = MutableLiveData(false)
    val pwdStatus:LiveData<Boolean> = _pwdStatus

    //手机号
    private val _phoneText = MutableLiveData("")
    val phoneText:LiveData<String> = _phoneText
    //手机号下标
    private val _phoneIndex = MutableLiveData(0)
    val phoneIndex:LiveData<Int> = _phoneIndex

    //密码框焦点
    private val _pwdFocus = MutableLiveData(false)
    val passwordFocus:LiveData<Boolean> = _pwdFocus
    //密码
    private val _passwordText = MutableLiveData("")
    val passwordText:LiveData<String> = _passwordText

    //登录按钮的状态
    private val _loginBtnStatus = MutableLiveData(false)
    val loginBtnStatus = _loginBtnStatus

    val phoneTextChange = object :TextWatcher{
        override fun beforeTextChanged(p0: CharSequence?, start: Int, p2: Int, p3: Int) {
        }

        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            val result = s.toString()
            var value = result.replace(" ","")
            if (value.length > 3){
                value = value.substring(0,3) + " " + value.substring(3)
            }
            if (value.length > 8){
                value = value.substring(0,8) + " " + value.substring(8)
            }
            _phoneText.value = value
            _phoneIndex.value = result.length
            //当长度大于13时,自动切换焦点给密码输入框
            if (value.length == 13){
                _pwdFocus.value = true
            }
            _loginBtnStatus.value =  (value.length > 12 && passwordText.value!!.length >= 6)
        }

        override fun afterTextChanged(p0: Editable?) {
        }
    }

     val pwdTextChange = object :TextWatcher{
        override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
        }

        override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
            _passwordText.value = p0.toString()
            _loginBtnStatus.value =  (phoneText.value!!.length > 12 && passwordText.value!!.length >= 6)
        }

        override fun afterTextChanged(p0: Editable?) {
        }
    }

    fun login(){
        viewModelScope.launch {
            val phone = phoneText.value?.replace(" ","")
            _loginResult.value = model.login(phone?:"",passwordText.value!!)
        }
    }

    fun controlPwd(){
        if ( _inputType.value == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD){
            _pwdStatus.value = false
            _inputType.value = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
        }else {
            _pwdStatus.value = true
            _inputType.value = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
        }
    }

    fun cleanPhone(){
        _phoneText.value = ""
    }

    fun cleanPassword(){
        _passwordText.value = ""
    }
}

4,创建对应的Model

import com.gaosi.databindingsimple.base.HttpResult
import kotlinx.coroutines.delay

class LoginRepository {

    suspend fun login(phone:String,password:String):HttpResult<String>{
        return if (phone == "0123456789000" && password == "123456"){
            delay(500)
            HttpResult.Success("登录成功")
        }else {
            HttpResult.Error("登录失败")
        }
    }
}

5,使用BindAdapter进行赋值

import android.text.TextWatcher
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import androidx.databinding.BindingAdapter

object BindingAdapters {

    @BindingAdapter("controlPwd")
    @JvmStatic fun controlPwd(view: ImageView, status: Boolean) {
        view.isSelected = status
    }

    @BindingAdapter("controlVisible")
    @JvmStatic fun controlVisible(view: ImageView, length: Int) {
        view.visibility = if (length > 0) View.VISIBLE else View.GONE
    }

    @BindingAdapter("addTextChangeListener")
    @JvmStatic fun addTextChangeListener(view:EditText,textWatcher: TextWatcher){
        view.addTextChangedListener(textWatcher)
    }

@BindingAdapter("addFocusChangeListener")
@JvmStatic fun addFocusChangeListener(view:EditText,hasFocus:Boolean){
    if (hasFocus){
        view.isFocusable = true
        view.isFocusableInTouchMode = true
        view.requestFocus()
    }
}

    @BindingAdapter("setDrawable")
    @JvmStatic fun setDrawable(view: ImageView,drawableId:Int){
        view.setImageDrawable(view.context.getDrawable(drawableId))
    }
}
三,主界面的实现

1,实现的效果

kotlin 配置ndk打包架构 kotlin+jetpack_MVVM_02


2,创建MainActivity并集成BaseActivity

import com.gaosi.databindingsimple.*
import com.gaosi.databindingsimple.adapter.ListAdapter
import com.gaosi.databindingsimple.base.*
import com.gaosi.databindingsimple.databinding.ActivityMainBinding
import com.gaosi.databindingsimple.entity.ListEntity

class MainActivity : BaseActivity<ActivityMainBinding,MainViewModel>() {

    private val list = ArrayList<ListEntity>()
    private val adapter  by lazy {
        ListAdapter(list)
    }

    override fun getLayoutId() = R.layout.activity_main

    override fun getVMClass() = MainViewModel::class.java

    override fun observerData() {
        super.observerData()
        binding.viewmodel = viewModel
    }

    override fun initView() {
        super.initView()
        binding.rvList.adapter = adapter
    }

    override fun initEvent() {
        super.initEvent()
        viewModel.listResult.observe(this){
            list.clear()
            list.addAll(it)
            adapter.notifyDataSetChanged()
        }
    }

    override fun loadData() {
        super.loadData()
        viewModel.getListData()
    }
}

3,创建对应的xml布局

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>
        <import type="android.view.View"/>
        <import type="com.gaosi.databindingsimple.UiState"/>
        <variable
            name="viewmodel"
            type="com.gaosi.databindingsimple.main.MainViewModel"/>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".main.MainActivity">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_list"
            android:layout_width="match_parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            android:orientation="vertical"
            tools:listitem="@layout/item_list"
            android:visibility="@{viewmodel.uiState == UiState.Success?View.VISIBLE:View.GONE}"
            android:layout_height="match_parent" />

        <ProgressBar
            android:id="@+id/cpb"
            android:layout_width="wrap_content"
            app:layout_constraintLeft_toLeftOf="parent"
            android:visibility="@{viewmodel.uiState == UiState.Loading?View.VISIBLE:View.GONE}"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_height="wrap_content" />

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/cl_empty"
            android:layout_width="match_parent"
            android:visibility="@{viewmodel.uiState == UiState.Failed?View.VISIBLE:View.GONE}"
            android:layout_height="match_parent">

            <ImageView
                android:id="@+id/iv_empty"
                android:layout_width="240dp"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                android:src="@mipmap/ic_launcher"
                app:layout_constraintBottom_toTopOf="@id/tv_empty"
                app:layout_constraintVertical_chainStyle="packed"
                android:layout_height="240dp" />

            <TextView
                android:id="@+id/tv_empty"
                android:layout_width="match_parent"
                app:layout_constraintTop_toBottomOf="@+id/iv_empty"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                android:layout_marginTop="24dp"
                android:gravity="center"
                android:textColor="@color/black"
                android:textSize="24sp"
                android:text="@{viewmodel.uiState == UiState.Failed ? @string/fails:@string/empty}"
                app:layout_constraintBottom_toBottomOf="parent"
                android:layout_height="wrap_content" />

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

4,创建主界面对应的ViewModel

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.gaosi.databindingsimple.UiState
import com.gaosi.databindingsimple.base.*
import com.gaosi.databindingsimple.entity.ListEntity
import kotlinx.coroutines.launch

class MainViewModel(application: Application) :AndroidViewModel(application) {
    val model by lazy {
        MainRepository()
    }

    private val _uiState = MutableLiveData(UiState.Loading)

    val uiState:LiveData<Int> = _uiState

    private val _listResult = MutableLiveData<List<ListEntity>>()

    val listResult :LiveData<List<ListEntity>> = _listResult

    fun getListData(){
        _uiState.value = UiState.Loading
        viewModelScope.launch {
            val result =  model.getListEntity("zhangsan")
            if (result is HttpResult.Success){
                _listResult.value = result.data as List<ListEntity>
                if (result.data != null){
                    _uiState.value = UiState.Success
                }else {
                    _uiState.value = UiState.Empty
                }
            }else {
                _uiState.value = UiState.Failed
            }
        }
    }
}

5,创建主界面的Model

data class ListEntity(val drawableId:Int,
                      val title:String?,
                      val subtitle:String?,
                      val showEdit:Boolean)
                      
                      
import android.util.Log
import com.gaosi.databindingsimple.R
import com.gaosi.databindingsimple.base.HttpResult
import com.gaosi.databindingsimple.entity.ListEntity
import kotlinx.coroutines.delay

class MainRepository {

   suspend fun getListEntity(userId:String):HttpResult<List<ListEntity>>{
       Log.d("MainRepository","getListEntity()--->$userId")
       delay(2000)
       val result = ArrayList<ListEntity>()
       for (i in 0..10){
           val entity = ListEntity(
               drawableId = R.mipmap.ic_launcher,
               title = "title:---->$i",
               subtitle = "subtitle:--->$i",
               showEdit = i %2==0
           )
           result.add(entity)
       }
       return HttpResult.Success(result)
    }
}

6,创建主界面对应的BindAdapter

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.RecyclerView
import com.gaosi.databindingsimple.R
import com.gaosi.databindingsimple.databinding.ItemListBinding
import com.gaosi.databindingsimple.entity.ListEntity

class ListAdapter(private val listItems:ArrayList<ListEntity>):
    RecyclerView.Adapter<ListAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
       val dataBinding:ItemListBinding =  DataBindingUtil.inflate(
           LayoutInflater.from(parent.context), R.layout.item_list, parent, false)
        return ViewHolder(dataBinding.root)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val dataBinding: ItemListBinding? = DataBindingUtil.getBinding(holder.itemView)
        dataBinding?.info = listItems[position]
        dataBinding?.executePendingBindings()
    }

    override fun getItemCount() = listItems.size

    class ViewHolder(itemView:View) :RecyclerView.ViewHolder(itemView)
}

7,对应使用的资源文件

<string name="empty">数据记录为空!</string>
<string name="fails">网络异常!</string>

该示例,实现的功能全部模拟正常项目中的需求使用MVVM的项目架构进行实现,主要用于MVVM的学习,功能覆盖的MVVM中的大部分的使用场景,其核心就是为了解耦复用的同时,使业务逻辑更加清晰,但缺点也很明显,如果不结合一些代码模板插件,反而会降低开发效率,针对一些功能单例的界面也没有必要非要按照MVVM的架构进行实现。
在真实的项目开发中,建议根据自己项目的特性抽离共性代码,生成相应的模板插件,可简化框架的实现,从而提升开发效率。