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