Android架构:MVVM实现离线登录

一、MVVM简介

MVVM模式是指Model-View-ViewModel。关于MVP架构,无论如何抽象化,在我们的View层中是无法避免的要处理一部分逻辑的。而MVVM模式中的View是将View的状态和行为完全抽象化,把逻辑与界面的控制完全交给ViewModel处理。

MVVM由下面三个核心组件组成:

  • Model: 用于获取业务数据模型
  • View: 定义了界面中的布局和外观
  • ViewModel: 逻辑控制层,负责处理数据和处理View层中的业务逻辑

二、离线登录逻辑梳理

1、打开软件,进入登录页面,检查用户数据库中是否有数据,如果有则自动加载用户名密码到输入栏。
2、用户输入用户名密码。
3、点击登录,比对用户输入用户名密码与数据库中密码是否一致,并进行登录结果提示。

三、搭建MVVM架构

1、Model层实现

这一层主要是获取数据库中的业务数据,本demo使用Room数据库,相比于GreenDao,Room更简洁高效,想深入了解Room搜索其他相关文章。

implementation 'android.arch.persistence.room:runtime:2.2.0'
annotationProcessor 'android.arch.persistence.room:compiler:2.2.0'

1.1、新建一个User实体

@Entity
public class User{
	@PrimaryKey(autoGenerate = true)
	@ColumnInfo(name = "uid")
	private int id;
	private String name;
	private String password;
	public User(String name,String password){
		this.name = name;
		this.password = password;
	}
	//...省略get set
}

用了@Entity标注的类,表示当前类的类名作为表名,这个类里面的所有属性,作为表里的字段。

1.2、创建UserDao

Dao为数据操作类,包含用于访问数据库的方法。

  • @Dao : 标注数据库操作的类。
  • @Query : 包含所有Sqlite语句操作。
  • @Insert : 标注数据库的插入操作。
  • @Delete : 标注数据库的删除操作。
  • @Update : 标注数据库的更新操作。
@Dao
public interface UserDao {
    //查询所有数据
    @Query("Select * from User")
    List<User> getAll();

    //删除全部数据
    @Query("DELETE FROM User")
    void deleteAll();

    //一次插入单条数据 或 多条
//    @Insert(onConflict = OnConflictStrategy.REPLACE),这个是干嘛的呢,下面有详细教程
    @Insert
    void insert(User... users);

    //一次删除单条数据 或 多条
    @Delete
    void delete(User... users);

    //一次更新单条数据 或 多条
    @Update
    void update(User... users);

    //根据字段去查找数据
    @Query("SELECT * FROM User WHERE id= :uid")
    Person getUserByUid(int uid);

    //一次查找多个数据
    @Query("SELECT * FROM UserWHERE id IN (:userIds)")
    List<Person> loadAllByIds(List<Integer> userIds);

    //多个条件查找
    @Query("SELECT * FROM UserWHERE name = :name AND password= :password")
    Person getUserByNameage(String name, int password);
}

这里唯一特殊的就是@Insert。其有一段介绍:对数据库设计时,不允许重复数据的出现。否则,必然造成大量的冗余数据。实际上,难免会碰到这个问题:冲突。当我们像数据库插入数据时,该数据已经存在了,必然造成了冲突。该冲突该怎么处理呢?在@Insert注解中有conflict用于解决插入数据冲突的问题,其默认值为OnConflictStrategy.ABORT。对于OnConflictStrategy而言,它封装了Room解决冲突的相关策略。

OnConflictStrategy.REPLACE:冲突策略是取代旧数据同时继续事务
OnConflictStrategy.ROLLBACK:冲突策略是回滚事务
OnConflictStrategy.ABORT:冲突策略是终止事务
OnConflictStrategy.FAIL:冲突策略是事务失败
OnConflictStrategy.IGNORE:冲突策略是忽略冲突

1.3、创建一个AppDataBase类

此类继承RoomDataBase,用于指定database的表映射实体数据以及版本等信息。

@Database(entities = {User.class}, version = 1,exportSchema = false)
public abstract class BaseAppData extends RoomDatabase {
    public abstract UserDao getUserDao();
}

1.4、创建数据库

public class DBInstance {
    //private static final String DB_NAME = "/sdcard/LianSou/room_test.db";
    private static final String DB_NAME = "room_test";
    public static AppDataBase appDataBase;
    public static AppDataBase getInstance(){
        if(appDataBase==null){
            synchronized (DBInstance.class){
                if(appDataBase==null){
                    appDataBase = Room.databaseBuilder(MyApplication.getInstance(),AppDataBase.class, DB_NAME)
                            //下面注释表示允许主线程进行数据库操作,但是不推荐这样做。
                            //我这里是为了Demo展示,稍后会结束和LiveData和RxJava的使用
                            .allowMainThreadQueries()
                            .build();
                }
            }
        }
        return appDataBase;
    }
}

需要使用时直接如下调用:

DBInstance.getInstance().getUserDao().insert(user);

2、ViewModel层实现

在ViewModel层处理业务逻辑。需要用到LiveData

public class LoginViewModel extends AndroidViewModel {
    private Context context;
    /**
     * 线程池
     */
    final ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

    public LoginViewModel(@NonNull Application application) {
        super(application);
    }

    /**
     * 登录
     *
     * @param context
     * @param user
     */
    public void doLogin(Context context, User user) {
        this.context = context;
        Runnable runnable = () -> {
            if (DBInstance.getInstance().getUserDao().getByUserNameAndPassword(user.getUserName(), user.getPassword()) != null) {
                //登录成功
                handler.sendEmptyMessage(0x56);
            }else {
                //登录失败
                handler.sendEmptyMessage(0x01);
            }
        };
        cachedThreadPool.execute(runnable);
    }

    /**
     * 获取用户账号信息
     *
     * @return
     */
    public MutableLiveData<User> getLastLoginAccount() {
        //因为用到LiveData,我觉得都不需要切换到主线程了。LiveData可以帮我们做
        //调用接口,返回我们的MutableLiveData<List<BannerBean>>
        final MutableLiveData<User> liveData = new MutableLiveData<>();
        cachedThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                User user = DBInstance.getInstance().getUserDao().getByUid(1);
                if(user == null){
                	user = new User();
                	user.setUserName("admin");
                	user.setPassword("123456");
                	getUserDao().insert(user);
                }
                liveData.postValue(user);
            }
        });
        return liveData;
    }

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == 0x56) {
                Toast.makeText(context, "登录成功", Toast.LENGTH_SHORT).show();   
            }else if (msg.what==0x01){
                Toast.makeText(context, "登录失败", Toast.LENGTH_SHORT).show();
            }
        }
    };
}

3、View层实现

在View层我们主要采用DataBinding将数据和view绑定,其中的优点和详细使用请自行百度,这一层主要是显示布局和外观等。
需要用到 Lifecycle

implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

3.1、创建xml布局文件

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

    <data>
    	<variable
            name="User"
            type="com.zhd.db.room.entities.User" />
            
        <variable
            name="onLoginClickListener"
            type="android.view.View.OnClickListener" />
    </data>

    <LinearLayout
    	android:orientation="vertical"
    	android:gravity="center_horizontal"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
		<EditText
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:hint="请输入您的账号"
            android:id="@+id/login_account"
            android:singleLine="true"
            android:text="@={User.name}"/>

		<EditText
			android:layout_marginTop="10dp"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:hint="请输入您的密码"
            android:inputType="textPassword"
            android:id="@+id/login_password"
            android:singleLine="true"
            android:text="@={User.password}"/>

		<Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{onLoginClickListener}"
            android:text="登录"/>
    </LinearLayout>
</layout>

3.2、创建BaseActivity

public abstract class BaseActivity<VM extends BaseViewModel, VDB extends ViewDataBinding> extends AppCompatActivity {

    /**
     * 获取当前activity布局文件,并初始化binding
     * @return
     */
    protected abstract int getContentViewId();

    /**
     * 处理逻辑业务
     */
    protected abstract void processLogic();


    protected VM mViewModel;
    protected VDB binding;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getContentViewId());
        //初始化binging
        binding = DataBindingUtil.setContentView(this, getContentViewId());
        //给binding加上感知生命周期,AppCompatActivity就是lifeOwner
        binding.setLifecycleOwner(this);
        //创建我们的ViewModel。
        createViewModel();
        processLogic();
    }

    public void createViewModel() {
        if (mViewModel == null) {
            Class modelClass;
            Type type = getClass().getGenericSuperclass();
            if (type instanceof ParameterizedType) {
                modelClass = (Class) ((ParameterizedType) type).getActualTypeArguments()[0];
            } else {
                //如果没有指定泛型参数,则默认使用BaseViewModel
                modelClass = BaseViewModel.class;
            }
            mViewModel = (VM) ViewModelProviders.of(this).get(modelClass);
        }
    }

3.3、创建LoginActivity

/**
 * ActivityLoginBinding 是DataBinding在xml页面绑定后自动生成的,【文件名】为:xml文件(名去掉_,且首字母大写)+ Binding,
 * 在build文件夹内,路径为 包名.+ databinding. + 【文件名】
 */
public class LoginActivity extends BaseActivity<LoginViewModel, ActivityLoginBinding> {

    @Override
    protected int getContentViewId() {
        return R.layout.activity_login;
    }

    @Override
    protected void processLogic() {
        //自动填充上次登录过的用户
        mViewModel.getLastLoginAccount().observe(LoginActivity.this, user ->
                binding.setUser(user)
        );
        binding.setOnLoginClickListener(v -> {
            //登录
            mViewModel.doLogin(LoginActivity.this,binding.getUser());
        });
    }
}

至此,MVVM框架已经简单地搭建起来了,能够实现真正的解耦,ViewModel层可以抽出来做单元测试,界面与数据的绑定也变得非常简单,代码量少了很多,结构清晰明了,便于开发与维护。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章