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層可以抽出來做單元測試,界面與數據的綁定也變得非常簡單,代碼量少了很多,結構清晰明瞭,便於開發與維護。

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