探究Google力推的JetPack库<三>---------LiveData&ViewModel再次探究、Room
LiveData&ViewModel再次探究:
DEMO编写:
关于LiveData和ViewModel的使用在之前https://www.cnblogs.com/webor2006/p/12483158.html已经学习过了,但是木有剖析它的实现原理,所以这里再次对它进行深入了解一下,先来编写两个DEMO:
具体效果如下:
①、利用ViewModel延迟5秒之后来更新UI:
②、利用LiveData和ViewModel来实现Fragment之间数据的实时更新:
而且我们在旋转屏幕时,数据也会被保留:
具体实现:
基本上都是之前学过的,所以这里直接将代码贴出既可:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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" android:orientation="vertical" tools:context=".MainActivity"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="click" android:text="demo1" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="click2" android:text="demo2" /> </LinearLayout>
package com.android.livedataandviewmodel; import android.os.Bundle; import android.view.View; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void click(View view) { //todo } public void click2(View view) { //todo } }
先来处理第一个点击事件:
然后先建立一个Model对像:
package com.android.livedataandviewmodel; import android.os.SystemClock; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import java.util.Timer; import java.util.TimerTask; public class LiveDataTimerViewModel extends ViewModel { private static final int ONE_SECOND = 5000; private MutableLiveData<Long> mElapsedTime = new MutableLiveData<>(); private long mInitialTime; public LiveDataTimerViewModel() { mInitialTime = SystemClock.elapsedRealtime(); Timer timer = new Timer(); // Update the elapsed time every second. timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { final long newValue = (SystemClock.elapsedRealtime() - mInitialTime) / 1000; // setValue() cannot be called from a background thread so post to main thread. mElapsedTime.postValue(newValue); } }, ONE_SECOND, ONE_SECOND); } public LiveData<Long> getElapsedTime() { return mElapsedTime; } }
上面代码就是做了个5秒的延时然后再去更新LiveData中的数据,比较简单,此时则回到Activity做一下监听:
package com.android.livedataandviewmodel; import android.os.Bundle; import android.widget.TextView; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProviders; public class ChronoActivity extends AppCompatActivity { private LiveDataTimerViewModel mLiveDataTimerViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.chrono_activity); mLiveDataTimerViewModel = ViewModelProviders.of(this).get(LiveDataTimerViewModel.class); subscribe(); } private void subscribe() { final Observer<Long> elapsedTimeObserver = new Observer<Long>() { @Override public void onChanged(@Nullable final Long aLong) { String newText = "123"; ((TextView) findViewById(R.id.timer_textview)).setText(newText); } }; //observeForever可以在activity不在前台也接收数据 mLiveDataTimerViewModel.getElapsedTime().observe(this, elapsedTimeObserver); } }
接下来再来处理demo2的点击事件:
它里面就是嵌套了2个Fragment:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".TestFragmentActivity"> <fragment android:id="@+id/fragment_one" android:name="com.android.livedataandviewmodel.FragmentOne" android:layout_width="match_parent" android:layout_height="200dp" /> <fragment android:id="@+id/fragment_two" android:name="com.android.livedataandviewmodel.FragmentTwo" android:layout_width="match_parent" android:layout_height="200dp" /> </LinearLayout>
package com.android.livedataandviewmodel; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProviders; public class FragmentOne extends Fragment { private EditText edContent; private Button btnSend; private NameViewModel model; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_one, container, false); edContent = view.findViewById(R.id.et_content); btnSend = view.findViewById(R.id.btn_send); //获取viewModel model = ViewModelProviders.of(getActivity()).get(NameViewModel.class); btnSend.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //获取到liveData后设置liveData的值 model.getmCurrentName().setValue(edContent.getText().toString()); } }); return view; } }
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <EditText android:id="@+id/et_content" android:layout_width="match_parent" android:layout_height="wrap_content" /> <Button android:id="@+id/btn_send" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="发送数据" /> </LinearLayout>
package com.android.livedataandviewmodel; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProviders; public class FragmentTwo extends Fragment { private TextView textName; private NameViewModel model; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_two, container, false); textName = view.findViewById(R.id.tv_text); //获取viewModel model = ViewModelProviders.of(getActivity()).get(NameViewModel.class); //监听值的变化 //model.getmCurrentName().observeForever();可以在任何时候得到数据 model.getmCurrentName().observe(getActivity(), new Observer<String>() { @Override public void onChanged(@Nullable String s) { //更新UI textName.setText(s); } }); return view; } }
<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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" android:orientation="vertical" tools:context=".MainActivity"> <TextView android:id="@+id/tv_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="显示数据" /> </LinearLayout>
这些代码都是比较简单的,就不过多的说明了,接下来重点是来剖析其原理。
原理剖析:
这里以分析两个Fragment之间的数据共享为例进行分析既可,那个延时的其本质使用是一样的,从这里分析起:
先来看一下这个of()方法:
这工厂是ViewModelProvider的内部类,很明显是用来创建ViewModel的,看一下它的getInstance()方法:
继续往下:
新建了一个ViewModelProvider,看一下它的构造方法:
此时看一下ViewModelStore是啥结构?
所以此时再回到主流程来:
继续往下:
此时咱们就来看一下工厂创建的细节:
从这反射可看出对于ViewModel会有一个Application参数的构造函数,如下:
此时我们的ViewModel就生成实例并存到ViewModelStore当中了,那有一个问题,为啥我们在旋转屏幕时的数据能够被保存下来呢?照理旋转时没有做特殊处理数据是会丢失的,所以接下来咱们来寻找答案:
对于屏幕旋转平常要处理数据保存都会到Activity的相应的回调中来处理,这里其实也是同样的,也是在Activity中来处理的,具体看一下:
此时看一下这个类的定义:
具体细节就不看了,接下来则来分析一下整个数据发送接收更新的逻辑,这块在之前https://www.cnblogs.com/webor2006/p/12483158.html已经分析过了,这里再过一遍:
如注释上所说,还有另外一个订阅方式:
咱们来看一下observe()的订阅细节:
好,接下来看一下修改LiveData数据的逻辑:
另外对于发送数据还有一种方式:
好,接下来集中看一下它里面的实现细节,怎么就通知到了观察者了:
这里关于这块消息的监听就分析到这了,比较好理解。
Room:
官方了解:
哦,是基于SQLite的,还是很有必要学一学的。
具体使用:
先添加依赖包:
基本使用:
然后先建立个实体类:
package com.android.room; public class Student { private int uid; private String name; private String password; private int addressId; public Student(String name, String password, int addressId) { this.name = name; this.password = password; this.addressId = addressId; } public int getUid() { return uid; } public void setUid(int uid) { this.uid = uid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getAddressId() { return addressId; } public void setAddressId(int addressId) { this.addressId = addressId; } @Override public String toString() { return "Student{" + "uid=" + uid + ", name='" + name + '\'' + ", password='" + password + '\'' + ", addressId=" + addressId + '}'; } }
光一个实全类怎么能跟表关联呢?此时注解就上来了,标注一个表则需要在类名上加上这个注解:
然后对于表肯定要有主键:
然后普通字段则:
其中要注意的是一定要给字段增加setter和getter方法才行,实体已经定义好了,接下来则定义DAO:
package com.android.room; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; import androidx.room.Query; import androidx.room.Update; import java.util.List; /** * 这就是数据访问对象 * 用于操作数据的API */ @Dao public interface StudentDao { @Insert void insert(Student... students); @Delete void delete(Student student); @Update void update(Student student); @Query("select * from Student") List<Student> getAll(); }
是不是这Dao的实现非常之精简,要平常我们还要写一大堆SQL语句才行,可以此框架的便捷之处。
接下来则需要定义数据库了:
好,接下来则可以来调用了,如下:
package com.android.room; import android.os.Bundle; import android.util.Log; import androidx.appcompat.app.AppCompatActivity; import androidx.room.Room; import java.util.List; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); DbTest t = new DbTest(); t.start(); } public class DbTest extends Thread { @Override public void run() { super.run(); //在这里进行数据库的操作 AppDatabase appDatabase = Room.databaseBuilder(getApplicationContext() , AppDatabase.class , "MyDB") .build(); StudentDao dao = appDatabase.userDao(); dao.insert(new Student("test", "111", 1)); dao.insert(new Student("test2", "222", 2)); dao.insert(new Student("test3", "333", 1)); dao.insert(new Student("test4", "444", 2)); List<Student> list = dao.getAll(); Log.e("cexo", list.toString()); } } }
输出:
2020-03-16 21:19:03.108 22029-22066/com.android.room E/cexo: [Student{uid=1, name='test', password='111', addressId=1}, Student{uid=2, name='test2', password='222', addressId=2}, Student{uid=3, name='test3', password='333', addressId=1}, Student{uid=4, name='test4', password='444', addressId=2}, Student{uid=5, name='test', password='111', addressId=1}, Student{uid=6, name='test2', password='222', addressId=2}, Student{uid=7, name='test3', password='333', addressId=1}, Student{uid=8, name='test4', password='444', addressId=2}]
那如果实体中有些是业务字段,不想参与到数据库表的字段中,则可以用一个注解来标识,类似于Realm数据库框架一样:
查询数据:
调用一下:
运行:
2020-03-16 21:36:10.085 23289-23550/com.android.room E/cexo: [Student{uid=1, name='test', password='111', addressId=1}, Student{uid=2, name='test2', password='222', addressId=2}, Student{uid=3, name='test3', password='333', addressId=1}, Student{uid=4, name='test4', password='444', addressId=2}, Student{uid=5, name='test', password='111', addressId=1}, Student{uid=6, name='test2', password='222', addressId=2}, Student{uid=7, name='test3', password='333', addressId=1}, Student{uid=8, name='test4', password='444', addressId=2}, Student{uid=9, name='test', password='111', addressId=1}, Student{uid=10, name='test2', password='222', addressId=2}, Student{uid=11, name='test3', password='333', addressId=1}, Student{uid=12, name='test4', password='444', addressId=2}] 2020-03-16 21:36:10.092 23289-23550/com.android.room E/cexo: Student{uid=1, name='test', password='111', addressId=1} 2020-03-16 21:36:10.096 23289-23550/com.android.room E/cexo: [Student{uid=2, name='test2', password='222', addressId=2}, Student{uid=3, name='test3', password='333', addressId=1}, Student{uid=4, name='test4', password='444', addressId=2}]
部分字段查询:
实际中有可能只会查询表中的某几个字段来形成一个新的对象,啥意思下面来演示一下,先建一个最终要生成的类,它的字段是只有Student表中的一部分,如下:
package com.android.room; import androidx.room.ColumnInfo; public class StudentTuple { @ColumnInfo(name = "name") public String name; @ColumnInfo(name = "pwd") public String password; public StudentTuple(String name, String password) { this.name = name; this.password = password; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "StudentTuple{" + "name='" + name + '\'' + ", password='" + password + '\'' + '}'; } }
可以看到它只有两个字段了,接下来咱们来查询一下:
调用一下:
运行:
2020-03-16 21:43:05.350 24229-24260/? E/cexo: [Student{uid=1, name='test', password='111', addressId=1}, Student{uid=2, name='test2', password='222', addressId=2}, Student{uid=3, name='test3', password='333', addressId=1}, Student{uid=4, name='test4', password='444', addressId=2}, Student{uid=5, name='test', password='111', addressId=1}, Student{uid=6, name='test2', password='222', addressId=2}, Student{uid=7, name='test3', password='333', addressId=1}, Student{uid=8, name='test4', password='444', addressId=2}, Student{uid=9, name='test', password='111', addressId=1}, Student{uid=10, name='test2', password='222', addressId=2}, Student{uid=11, name='test3', password='333', addressId=1}, Student{uid=12, name='test4', password='444', addressId=2}, Student{uid=13, name='test', password='111', addressId=1}, Student{uid=14, name='test2', password='222', addressId=2}, Student{uid=15, name='test3', password='333', addressId=1}, Student{uid=16, name='test4', password='444', addressId=2}, Student{uid=17, name='test', password='111', addressId=1}, Student{uid=18, name='test2', password='222', addressId=2}, Student{uid=19, name='test3', password='333', addressId=1}, Student{uid=20, name='test4', password='444', addressId=2}] 2020-03-16 21:43:05.356 24229-24260/? E/cexo: Student{uid=1, name='test', password='111', addressId=1} 2020-03-16 21:43:05.359 24229-24260/? E/cexo: [Student{uid=2, name='test2', password='222', addressId=2}, Student{uid=3, name='test3', password='333', addressId=1}, Student{uid=4, name='test4', password='444', addressId=2}] 2020-03-16 21:43:05.363 24229-24260/? E/cexo: [StudentTuple{name='test', password='111'}, StudentTuple{name='test2', password='222'}, StudentTuple{name='test3', password='333'}, StudentTuple{name='test4', password='444'}, StudentTuple{name='test', password='111'}, StudentTuple{name='test2', password='222'}, StudentTuple{name='test3', password='333'}, StudentTuple{name='test4', password='444'}, StudentTuple{name='test', password='111'}, StudentTuple{name='test2', password='222'}, StudentTuple{name='test3', password='333'}, StudentTuple{name='test4', password='444'}, StudentTuple{name='test', password='111'}, StudentTuple{name='test2', password='222'}, StudentTuple{name='test3', password='333'}, StudentTuple{name='test4', password='444'}, StudentTuple{name='test', password='111'}, StudentTuple{name='test2', password='222'}, StudentTuple{name='test3', password='333'}, StudentTuple{name='test4', password='444'}]
定义外键结束:
这里假设给这个字段建立外键约束:
然后新建对应的实体:
package com.android.room; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey; @Entity public class Address { @PrimaryKey(autoGenerate = true) public int addressId; @ColumnInfo(name = "addressName") public String name; public Address(int addressId, String name) { this.addressId = addressId; this.name = name; } public int getAddressId() { return addressId; } @Override public String toString() { return "Address{" + "addressId=" + addressId + ", name='" + name + '\'' + '}'; } public void setAddressId(int addressId) { this.addressId = addressId; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
此时在数据库中就需要增加一张表的定义了,如下:
此时运行报错了。。
2020-03-16 21:51:14.298 24936-24970/? E/AndroidRuntime: FATAL EXCEPTION: Thread-2 Process: com.android.room, PID: 24936 java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number. at androidx.room.RoomOpenHelper.checkIdentity(RoomOpenHelper.java:154) at androidx.room.RoomOpenHelper.onOpen(RoomOpenHelper.java:135) at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.onOpen(FrameworkSQLiteOpenHelper.java:142) at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:409) at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:298) at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(FrameworkSQLiteOpenHelper.java:92) at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:53) at androidx.room.RoomDatabase.inTransaction(RoomDatabase.java:476) at androidx.room.RoomDatabase.assertNotSuspendingTransaction(RoomDatabase.java:281) at com.android.room.StudentDao_Impl.insert(StudentDao_Impl.java:91) at com.android.room.MainActivity$DbTest.run(MainActivity.java:31)
意思是说增加表了的话则需要提升数据库的版本号,所以:
具体这块就不运行了。
引用类型:
有时候可能实体中会引用其它实体,那该怎么定义呢?
LiveData、RxJava、Cursor的查询类型:
对于查询其实还可以支持其它几种类型,比如:
可见是非常之灵活的。
数据升级迁移处理:
这块就如我们之前Sqlite中的onUpgrade()方法,这里是这样处理的:
原理剖析:
其实它的实现原理就是通过注解处理器来实现的,瞅一下:
其中跟进去最终就是调用的Sqlite的API了:
而另一个DB相关的生成类中则是跟创建数据表相关了,大致瞅一下:
当然具体的细节还是很多的,这里只掌握核心原理既可,关于它的使用还得在实际项目中进行熟练,这里算是对它进行一个入门。