Android第一行代码第三版 第十三章读书笔记——Jetpack

第十三章 高级程序开发组件,探究Jetpack(第三版)

2017年Google推出了一个官方的架构组件库——Architecture Components,旨在帮助开发者编写出更加符合高质量代码规范、更具有架构设计的应用程序。

2018年Google推出了一个全新的开发组件工具及Jetpack,并将Architecture Components纳入其中。

ViewModel

ViewModel的一个重要作用就是可以帮助Activity分担一部分工作,它是专门用于存放于界面相关的数据的。只要是界面上能看到的数据,它的相关变量就应该存放在ViewModel中,而不是Activity中。

ViewModel只是用来管理UI的数据的,千万不要让它持有View、Activity或者Fragment的引用(小心内存泄露)

当手机发生横竖屏旋转的时候Activity会被重建,同时存放在Activity中的数据与也会丢失。而ViewModel不会被重建,只有当Activity退出的时候才会跟着Activity一同销毁。

ViewModel的基本用法

使用ViewModel组件,需要在app/build.gradle文件中添加如下依赖:

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

比较好的编程规范时给每一个Activity和Fragment都创建一个对应的ViewModel。

创建的常用方式

由于ViewModel的生命周期是由系统维护的,因此不能直接在代码中通过new的方式创建。

  1. 直接用 ViewModelProvider 获取

    viewModel = new ViewModelProvider(this).get(MainViewModel.class);

  2. 通过实现ViewModelFactory接口创建

    • 创建一个类实现ViewModelProvider.Factory接口

      public class VMFactory implements ViewModelProvider.Factory {
          
          @NonNull
          @Override
          public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
              return (T) new MainViewModel();
          }
      }

    • 在MainActivity例获取实例

      viewModel = new ViewModelProvider(this, new VMProvider()).get(MainViewModel.class);

这两种方式都是基于ViewModelProvider.Factory来生成ViewModel的实例,只不过第一种方式如果加Factory参数,会使用内部默认的Factory。

我们要实现一个计数器的功能。

给MainActivity创建一个对应的MainViewModel类,让这个类继承于ViewModel,然后在这个类中加入所有与界面相关的数据。

public class MainViewModel extends ViewModel {
    public int counter = 0;
}

修改activity_main.xml文件中的代码

<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"
    tools:context=".MainActivity">
​
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/infoText"
        android:layout_gravity="center_horizontal"
        android:textSize="32sp"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/plusOneBtn"
        android:layout_gravity="center_horizontal"
        android:text="Plus One"/>
​
</LinearLayout>

添加了TextView用来显示计数。添加了一个Button用来使计数加一。

接着实现计数器的逻辑,编写MainActivity中的代码。

public class MainActivity extends AppCompatActivity {
    private MainViewModel viewModel;
    private Button poBtn;
    private TextView infoText;
​
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
//        viewModel = new ViewModelProvider(this).get(MainViewModel.class);
        viewModel = new ViewModelProvider(this, new VMProvider()).get(MainViewModel.class);
        poBtn = (Button) findViewById(R.id.plusOneBtn);
        infoText = (TextView) findViewById(R.id.infoText);
        poBtn.setOnClickListener(view -> {
            viewModel.counter++;
            refreshCounter();//刷新计数器显示的内容
        });
    }
​
​
    private void refreshCounter() {
        //刷新
        String text = viewModel.counter + "";
        infoText.setText(text);
    }
}

之所以不能在MainActivity中使用new MainViewModel创建实例。因为ViewModel有独立的生命周期,并且它的生命周期应该长于Activity。如果在MainActivity中直接创建ViewModel的实例,当手机屏幕旋转时,MainActivity会重新创建一个新的实例,原来的数据就无法保留。

所以要通过ViewModelProvider来获取实例

这样就做到了旋转手机屏幕显示不会丢失数据。

向ViewModel传递参数

我们实现了旋转手机屏幕不会丢失数据。但退出程序后这个计数还是会重新开始。

  1. 修改MainViewModel类的代码

    public class MainViewModel extends ViewModel {
        public int counter = 0;
    ​
        public MainViewModel() {
        }
    ​
        public MainViewModel(int counterReserved) {
            this.counter = counterReserved;
        }
    }

  2. 新建一个类实现ViewModel.Factory接口

    public class VMProvider implements ViewModelProvider.Factory {
    ​
        private int countReserved = 0;
    ​
        public VMProvider(int countReserved) {
            this.countReserved = countReserved;
        }
    ​
        public VMProvider() {
    ​
        }
    ​
        @NonNull
        @Override
        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
            return (T)new MainViewModel(countReserved);
        }
    }

这两个类都写了两个构造方法,如果调用空参的方法获取ViewModel的实例就是从0开始计数。如果调用含参的方法就是保持之前的结果开始计数。

  1. 修改activity_main.xml文件中的代码

    <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"
        tools:context=".MainActivity"
        android:orientation="vertical">
    ​
        ...
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/clearBtn"
            android:text="Clear" />
    </LinearLayout>

    添加了一个按钮用来让用户手动清零。

  2. 修改MainActivity中的代码

    public class MainActivity extends AppCompatActivity {
        ...
        private Button clearBtn;
        private SharedPreferences sp;
    ​
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    ​
            sp = getPreferences(Context.MODE_PRIVATE);
            int countReserved = sp.getInt("count_reserved", 0);//取出保存的数据,如果没有返回默认值为0
    ​
    //        viewModel = new ViewModelProvider(this).get(MainViewModel.class);
            viewModel = new ViewModelProvider(this, new VMProvider(countReserved)).get(MainViewModel.class);
            ...
            clearBtn = (Button) findViewById(R.id.clearBtn);
            clearBtn.setOnClickListener(view -> {
                viewModel.counter = 0;
                refreshCounter();//刷新计数器显示的内容
            });
            refreshCounter();
        }
    ​
        //从写onPause()方法对计数器当前数据进行保存,在活动与用户停止交互的时候就保存数据,不会丢失数据
        @Override
        protected void onPause() {
            super.onPause();
            SharedPreferences.Editor editor = sp.edit();//获取SharedPreferences.Editor的对象
            editor.putInt("count_reserved", viewModel.counter);
            editor.apply();
        }
    ​
        private void refreshCounter() {
            //刷新
            String text = viewModel.counter + "";
            infoText.setText(text);
        }
    }

这样无论是退出程序还是进入后台,数据都不会丢失。

Lifecycles

手写监听器感知Activity的生命周期

通过手写监听器的方式来对Activity的生命周期进行感知:

Lifecycles的使用

  • 创建观察者

  • 注册观察者

Lifecycle的作用是在活动和碎片外感知Activity的生命周期。

Lifecycle的应用场景

Android——Lifecycles的学习_android lifecycles_Liquor...的博客-CSDN博客

创建观察者

  1. 创建类实现LifecycleObserve接口

    public class MyObserve implements LifecycleObserver {
        //LifecycleObserve是一个空接口,只需要实现,而不需要重写任何方法。
    ​
        //方法的参数可以传LifecycleOwner,也可以不写
        //方法要加上注解@OnLifecycleEvent(Lifecycle.Event.XXX),可以让该方法监听到注解生命周期的变化
        //这个注解现在已经弃用,官方推荐让观察者类直接实现DefaultLifecycleObserver接口
        
        @OnLifecycleEvent(Lifecycle.Event.ON_START)//在Activity的onStart()回调时执行
        private void activityStart(LifecycleOwner owner) {
            Log.d("TAG", "activityStart: ");
        }
    ​
        @OnLifecycleEvent(Lifecycle.Event.ON_STOP)//在Activity的onStop()回调时执行
        private void activityStop() {
            Log.d("TAG", "activityStop: ");
        }
    }

    在方法上使用了@OnLifecycleEvent注解,并传入了一种生命周期时间。生命周期事件的类型一共有七种:ON_CREATE、ON_START、ON_RESUME、ON_PAUSE、ON_STOP、ON_DESTROY分别匹配Activity中相应的生命周期回调;另外还有一种ON_ANY的类型,表示可以匹配Activity的任何生命周期回调。

    @OnLifecycleEvent 遭废弃,推荐使用 DefaultLifecycleObserver 替代_fundroid的博客-CSDN博客

注册观察者

AppCompatActivity与Fragment是默认的被观察者,在其内部通过调用getLifecycle().addObserver(观察者)注册定义好的观察者即可。

只要你的Activity是继承自AppCompatActivity的,或者你的Fragment是继承自androidx.fragment.app. Fragment的,那么它们本身就是一个LifecycleOwner的实例,这部分工作已由AndroidX库自动帮我们完成了。

我们只需要在所要监听的活动或者碎片添加如下代码即可。

getLifecycle().addObserver(new MyObserve());//注册观察者

效果:先将应用打开再关闭。

先后出现两句日志。

主动获知当前的生命周期状态

这就是Lifecycle组件最常见的用法了。不过目前MyObserver虽然可以感知到Activity的生命周期发生了变化,却没办法主动获知当前的生命周期状态。

使用Lifecycle对象的getCurrentState()方法可以在活动和碎片的任何地方获知到当前的生命周期状态。

Lifecycle.State state = getLifecycle().getCurrentState();
Log.d("TAG", state.toString() );

想在活动或碎片外的地方感知当前的生命周期状态。可以给MyObserver类添加一个变量,在构造方法中接收并保存一个Lifecycle的对象。

MyObserver类中:

public class MyObserver implements LifecycleObserver {
    ...
    private Lifecycle lifecycle;
    public MyObserver(Lifecycle lifecycle) {
        this.lifecycle = lifecycle;
    }
​
    public Lifecycle getMyLifecycle() {
        return lifecycle;
    }
}

MainActivity中:

getLifecycle().addObserver(new MyObserver(getLifecycle()));//注册观察者

注册观察者的时候将本活动的Lifecycle对象交给观察者保管。

这样就算活动外,也可以通过MyObserver的对象再调用getCurrentState()方法主动感知当前的生命周期状态。

getCurrentState()方法

这个方法的返回值是一个枚举类型,一共有INITIALIZED、DESTROYED、CREATED、STARTED、RESUMED这5中状态类型,他们与Activity的生命周期回调所对应的关系如图。

LiveData

LiveData是Jetpack提供的一种响应式编程组件,它可以包含任何类型的数据,并在数据发生变化的时候通知给观察者。LiveData特别适合与ViewModel结合在一起使用。

LiveData的基本用法

前面介绍ViewModel时编写的计数器其实存在问题。

当点击按钮,计数器加一,之后会立即获取最新的计数返回到UI上。这种方式在单线程模式下确实可以正常工作,但如果ViewModel的内部开启了线程去执行一些耗时的逻辑。那么获取并返回到UI上的计数可能会是之前的数据。counter耗时线程中还没有改变。

问题是如何让将ViewModel的数据变化主动通知给Activity。

这里一定不能把Activity的实例传给ViewModel。因为Activity的生命周期比ViewModel的生命周期更短,可能导致Activity无法及时释放而造成内存泄漏。

应该使用LiveData解决。

修改MainViewModel中的代码:

public class MainViewModel extends ViewModel {
    public MutableLiveData<Integer> counter = new MutableLiveData<Integer>();
    //计数器变为MutableLiveData类型的
​
    public MainViewModel() {
    }
​
    public MainViewModel(int counterReserved) {
        this.counter.setValue(counterReserved);
    }
​
    public void plusOne() {
        int count = counter.getValue() == null ? 0 : counter.getValue();
        //将当前技术其中的数取出来,如果是null就设置默认值为0
        counter.setValue(count + 1);
        //加一存回去
    }
​
    public void clear() {
        counter.setValue(0);
        //直接设置为0
    }
}

LivaData主要有三个方法用于读写数据:getValue()方法yonyuhuoquLiveData中包含的数据;setValue()方法用于给LiveData设置数据,但只能在主线程中调用,在UI线程中调用该方法通知数据变更;postValue()方法用于在非主线程中给LiveData设置数据,在子线程中调用该方法通知数据变更,该方法中切换到UI线程后调用setValue方法

修改MainActivity中的代码:

public class MainActivity extends AppCompatActivity {
    ...
​
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        poBtn = (Button) findViewById(R.id.plusOneBtn);
        infoText = (TextView) findViewById(R.id.infoText);
        poBtn.setOnClickListener(view -> {
            viewModel.plusOne();
        });//增加计数的按钮
        clearBtn = (Button) findViewById(R.id.clearBtn);
        clearBtn.setOnClickListener(view -> {
            viewModel.clear();
        });//清空计数的按钮
​
        viewModel.counter.observe(this, new Observer<Integer>() {
            //第一个参数接受一个LifecycleOwner的对象,Activity本身就是一个LifecycleOwner的对象
            //第二个参数是Observer接口
            //LivaData的数据发生变化时就会执行onChanged()方法
            @Override
            public void onChanged(Integer integer) {
                refreshCounter();
            }
        });//观察数据变化的方法
    }
​
    //从写onPause()方法对计数器当前数据进行保存,在活动与用户停止交互的时候就保存数据,不会丢失数据
    @Override
    protected void onPause() {
        super.onPause();
        SharedPreferences.Editor editor = sp.edit();//获取SharedPreferences.Editor的对象
        editor.putInt("count_reserved", viewModel.counter.getValue() == null ? 0 : viewModel.counter.getValue());
        editor.apply();
    }
​
    private void refreshCounter() {
        //刷新
        String text = viewModel.counter.getValue() + "";
        infoText.setText(text);
    }
}

我们把其他地方的refreshConter()方法删掉。让LiveData帮我们监视计数是否发生变化。通过LiveData的observe()方法设置监视。如果发生了变化会执行第二个参数接口中的重写方法onChanged(),在这里面调用刷新方法refreshCounter()。

然后将添加、清空、刷新等操作使用LivaData的三个读写数据的方法修改。

要注意在子线程中修改数据一定要使用postValue()方法。

拓展

Android推荐写法

为了保证ViewModel书的封装性。官方推荐永远只暴露不可变的LiveData 给外部。这样在非ViewModel 中就只能观察 LiveData 的数据变化,而不能给LiveData 设置数据。

修改MainViewModel中的代码:

public class MainViewModel extends ViewModel {
    private MutableLiveData<Integer> _counter = new MutableLiveData<Integer>();
​
    public final LiveData<Integer> counter = _counter;
​
    public MainViewModel() {
    }
​
    public MainViewModel(int counterReserved) {
        this._counter.setValue(counterReserved);
    }
​
    public void plusOne() {
        int count = counter.getValue() == null ? 0 : counter.getValue();
        _counter.setValue(count + 1);
    }
​
    public void clear() {
        _counter.setValue(0);
    }
}

这里先将原来的counter变量名改为_counter变量,并给它加上private修饰符。这样数据就是对外不可见的了。

然后我们由重新定义了一个counter变量,将它的类型声明为不可变的LiveData。

Kotlin:

包装后的LiveData只能使用getValue()获取数据,而不能使用setValue()和postValue()修改数据。

map和switchMap(陌生)

两种转化方法。

map()方法

map()方法的作用是将实际包含数据的LiveData和仅用于观察数据的LiveData进行转换。

应用场景:假设现在有一个User类,类中包含用户的姓名和年龄字段。

如果MainActivity中明确只会显示用户的姓名,而完全不关心用户的年龄。想要避免将整个LiveData的数据暴露出去,而是只暴露姓名。

map()方法可以将User类型的LiveData自由的转型成任意其它类型的LiveData。

public class UserViewModel extends ViewModel {
    private final MutableLiveData<User> userLiveData = new MutableLiveData<>();
    public LiveData<String> userName = Transformations.map(userLiveData, new Function<User, String>() {
        @Override
        public String apply(User input) {
            return input.getName();
        }
    });
    ...
}
//可以看到这里我们调用了Transformations的map()方法来对LivaData的数据类型进行转换。map方法接受两个参数,第一个是原始的LiveData;第二个参数是一个转换函数。我们在转换函数里编写具体的转换逻辑即可。当userLiveData的数据变动时,会自动调用这个方法更新转换
class User {
    private String name;
    private int age;
​
    public String getName() {
        return name;
    }
​
    public int getAge() {
        return age;
    }
}
switchMap()方法

应用场景:ViewModel中的某个LiveData对象是调用另外的方法获取的,并不是直接在ViewModel中创建的实例。假如现在有一个Repository单例类。

class Repository {
    public LiveData<User> getUser() {
        MutableLiveData<User> liveData = new MutableLiveData<>();
        liveData.setValue(new User());
        return liveData;
    }
}

这里我们在这个Repository类中添加了一个getUser()方法,按照正常的逻辑,我们应该传入一个useId参数去服务器请求或者到数据库中查找相应的User对象,但是这里只是模拟实例,因此每次将传入的userId当作用户姓名来创建一个新的User对象即可。这里参数userId也省略了,直接创建空参的User对象。

getUser()方法返回的是一个包含User数据的LiveData对象,而且没调用一次都会返回一个新的LiveData实例。

public class UserViewModel extends ViewModel {
    ...
    public LiveData<User> getUser() {
        return Repository.getUser();
    }
}

我们在UserViewModel中也编写一个getUser()方法,并让他去调用Repository的getUser()方法来获取一个新的LiveData对象。

如果ViewModel中的某个LiveData对象是调用另外的方法获取的,那么我们就可以借助switchMap()方法,将这个LiveData对象转换成另外一个可观察的LiveData对象。

修改UserViewModel中的代码:

public class UserViewModel extends ViewModel {
    ...
    
    private final MutableLiveData<String> userIdLivaData = new MutableLiveData<>();
    LiveData<User> user = Transformations.switchMap(userIdLivaData, new Function<String, LiveData<User>>() {
        @Override
        public LiveData<User> apply(String input) {
            return Repository.getUser();
        }
    });
    //Transformations.switchMap()
    /*
    当userIdLiveData的数据变动就会自动调用这个方法更新
    第一个参数:传入我们新增的userIdLiveData
    第二个参数:是一个转换函数,返回一个可观察的LivaData对象
    */
    
    public void getUser(String userId) {
        userIdLivaData.setValue(userId);
    }
    
}

先定义了一个userIdLivaData对象,用来观察userId的数据变化,然后调用了Transformations的switchMap()方法,用来对另一个可观察的LiveData对象进行转换。

userIdLiveData就是我们调用方法从外部获取的LiveData数据,使用switchMap()方法转换成了user可观察的数据。

当外部调用UserViewModel的getuser()方法来获取用户数据时,并不会发起任何请求或者函数调用,只会讲传入的userId设置到useridLiveData中。一旦userIdLiveData的数据发生变化,那么观察userIdLiveData的switchMap()方法就会执行,并且调用我们编写的转换函数。然后再转换函数中调用Repository.getUser()方法获取到真正的用户数据。同时switchMap()方法会将Repository.getUser()方法返回的LiveData对象转换成一个可观察的LiveData对象。在MainActivity中观察这个对象即可。(user)

没看懂。

Room

Android数据库的ORM框架。也叫对象关系映射。就是将面向对象的语言和面向关系的数据库之间建立一种映射关系。

ORM框架可以用面向对象的思维来和数据库进行交互。

Android官房推出了一个ORM框架并加入到 Jetpack 中,就是 Room

Room是一个持久性数据库。

Room持久性库提供了SQLite的抽象层,以便在充分利用SQLite的同时允许流畅的数据库访问,利用SQlite的全部功能。

使用Room进行增删改查

Room的整体结构
  • Entity。用于定义封装实际数据的实体类,每个实体类都会在数据库中有一张对应的表,并且表中的列是根据实体类中的字段自动生成的。

  • Dao。是数据访问对象的意思,通常会在这里对数据库的各项操作进行封装,在实际编程的时候就不需要与底层数据库打交道了,直接和Dao层进行交互即可。

  • Database。用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提供Dao层的访问实例。

导包:

android 中Room 的简单使用android room注解sinat_bond的博客-CSDN博客

Entity实体类

首先定义实体类。

@Entity
//注解@Entity将这个各类声明成一个实体类
public class User {
    @ColumnInfo(name = "name")
    private String name;
    //注解@ColumnInfo代表列的数据,name可以给该列命名
    @ColumnInfo(name = "age")
    private int age;
    @PrimaryKey(autoGenerate = true)
    //注释@PrimaryKey表示主键 将autoGenerate设置为true表示让主键自增
    private long id;
}

Dao数据访问对象

Room用法中最关键的地方,封装访问数据库的操作。

新建一个userDao接口,注意必须使用接口。

@Dao
public interface UserDao {
    @Query("select * from User")
    //查询 @Query注解
    List<User> loadAllUser();
​
    @Query("select * from User where age > :userAge")
    //查询age大于参数userAge的用户
    List<User> loadUsersOlderThan(int userAge);
    
    @Query("delete from User where name = :userName")
    //删除用户名为参数userName的用户
    //如果使用非实体类(对象)参数,要用@Query注解找到要操作的对象,在括号里进行相应操作
    int deleteByLastName(String userName);
    
    @Delete
    //删除
    void deleteUser(User user);
    
    @Insert
    //插入、添加 加入数据库之后返回主键id值
    long insertUser(User user);
    
    @Update
    //更新
    void updateUser(User user);
}

UserDao接口上面使用了一个@Dao注解,这样Room才能将他识别为一个Dao。Dao内部是对数据库的操作进行封装,数据库常用的操作有增删改查,所以Room也提供了这四种注解。

Database数据库

定义数据库类一定要满足下面条件:

  • 该类必须带有@Database注解,该注解包含列出所有与数据库关联的数据实体的entities数组。

  • 该类必须是一个抽象类,用于拓展RoomDatabase

  • 对于与数据库关联的每个Dao,数据库必须定义一个具有零参

定义好三个部分的内容:

  • 数据库的版本号

  • 包含哪些实体类

  • 以及提供Dao层的访问实例

新建一个AppDatabase文件,编写如下代码:

@Database(version = 1, entities = {User.class})
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
​
    //定义抽象方法,用于获取Dao类的实例
    //只需要进行方法的声明,具体的方法实现是由Room在底层自动完成的。
    //有几个Dao层,就应该写几个抽象方法。
    private static AppDatabase instance = null;
​
    public static AppDatabase getInstance(Context context) {
        if (instance == null) {
            Builder<AppDatabase> database = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "database");
            /*
            * Room.databaseBuilder()方法接收三个参数
            * 第一个参数是一个applicationContext,不能是context。
            * 第二个参数是AppDatabase的class对象
            * 第三个参数是数据库名
            * 最后调用build()方法创建数据库的实例。
            * */
            instance = database.build();
        }
        return instance;
    }
​
}

AppDatabase类的头部使用了@Database注解,并在注解中声明了版本号和包含的实体类,如果有多个实体类可以用逗号隔开。

将AppDatabase定义成抽象类,并继承于RoomDatabase,然后提供相应的抽象方法返回Dao层的实例。

至此一个Database数据库就建立好了。

补充AppDatabase类,将这个类设置成单例模式。因为通常一个数据库在全局只需要一个实例。定义了一个私有变量instance,和一个公开方法getInstance(),如果instance为空,就调用Room.databaseBuilder()方法创建一个数据库的实例,并保存在instance里返回;如果instance不为空则直接返回这个instance。

进行测试

修改activity_main.xml文件中的代码

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:orientation="vertical">
​
    ...
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/addDataBtn"
        android:text="Add Data"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/updateDataBtn"
        android:text="Update Data"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/deleteDataBtn"
        android:text="Delete Data"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/queryDataBtn"
        android:text="Query Data"/>
</LinearLayout>

添加了4个按钮y哦那个与增删改查4个操作。

然后修改MianActivity中的代码

public class MainActivity extends AppCompatActivity {
    ...
​
    @SuppressLint("MissingInflatedId")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
​
        //首先获取了UserDao的实例,并创建两个User对象。
        UserDao userDao = AppDatabase.getInstance(this).userDao();//获取到Dao层实例
        User user1 = new User("张三", 23);
        User user2 = new User("李四", 24);
​
        //然后在“Add Data”按钮中添加点击事件。调用UserDao的insertUser()方法
        findViewById(R.id.addDataBtn).setOnClickListener(view -> {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    user1.id = userDao.insertUser(user1);
                    user2.id = userDao.insertUser(user2);
                }
            }).start();
        });
        findViewById(R.id.updateDataBtn).setOnClickListener(view -> {
            new Thread(() -> {
                user1.age = 42;
                userDao.updateUser(user1);
            }).start();
        });
        findViewById(R.id.deleteDataBtn).setOnClickListener(view -> {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    userDao.deleteByLastName("张三");
                }
            }).start();
        });
        findViewById(R.id.queryDataBtn).setOnClickListener(view -> {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (User user:
                         userDao.loadAllUser()) {
                        Log.d("TAG", user.toString());
                    }
                }
            }).start();
        });
    }
    ...
}

因为数据库操作属于耗时操作,,Room默认是不允许在主线程中进行数据操作的。因此创建了子线程进行数据库操作。

不过为了方便测试,Room还提供了一个更简单的方法。

Builder<AppDatabase> database = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "database").allowMainThreadQueries();
instance = database.build();

在原先勾线AppDatabase实例的 Room.databaseBuilder() 方法后面加上allowMainThreadQueries()方法,这样Room就郧西在主线程中进行数据库操作了,这个方法之间以在测试环境下使用。

Room的数据库升级

Builder<AppDatabase> database = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "database")
                    .fallbackToDestructiveMigration();
​
            instance = database.build();

在构建AppDatabase实例的时候,在Room.databaseBuilder()方法后加上fallbackToDestructiveMigration()方法。这样只要数据库进行升级,Room就会将当前的数据库销毁,然后再重新创建。但之前数据库中的数据全部丢失了。

如果程序还在开发阶段,这个方法是可以用的。

正规写法

在数据库中添加一张表Book。

  1. entity实体类

    @Entity
    public class Book {
        @PrimaryKey(autoGenerate = true)
        public long id;
        @ColumnInfo(name = "name")
        public String name;
        @ColumnInfo(name = "pages")
        public int pages;
    }

  2. Dao层接口

    @Dao
    public interface BookDao {
        @Insert
        long insertBook(Book book);
        @Query("select * from Book")
        List<Book> loadAllBooks();
    }

  3. 修改AppDatabase中的代码。

    @Database(version = 2, entities = {User.class, Book.class})
    public abstract class AppDatabase extends RoomDatabase {
        public abstract UserDao userDao();
        public abstract BookDao bookDao();
    ​
        public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
            //参数表示数据库从版本1升级到2
            @Override
            public void migrate(@NonNull SupportSQLiteDatabase database) {
                database.execSQL("create table Book (id integer primary key autoincrement not null, name text not null, pages integer not null)");
            }
        };
    ​
        private static AppDatabase instance = null;
    ​
        public static AppDatabase getInstance(Context context) {
            if (instance == null) {
                Builder<AppDatabase> database = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "database")
                        .addMigrations(MIGRATION_1_2);
    ​
                instance = database.build();
            }
            return instance;
        }
    ​
    }

改动的地方:1.注解@Database的版本号由1变成了2。

2.注解@Database的实体类中添加了Book.class。

3.抽象类AppDatabase中添加了获取BookDao的实例的抽象方法。

4.定义了一个Migration类型的变量,实现了Migration()匿名内部类,在重写方法migrate()中使用了建表语句。

5.在Room.databaseBuilder()方法后加上了addMigration()方法,并传入Migration变量作为参数。

如果数据库升级时想要修改的时表中的一列,而不是整张表。使用alter语句修改表结构就可以了。

Book实体类中:

@Entity
public class Book {
    @PrimaryKey(autoGenerate = true)
    public long id;
    @ColumnInfo(name = "name")
    public String name;
    @ColumnInfo(name = "pages")
    public int pages;
    @ColumnInfo(name = "author")
    public String author;
}

我们添加了一个作者字段,表示这张表添加一列。

修改AppDatabase中的代码:

@Database(version = 3, entities = {User.class, Book.class})
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
    public abstract BookDao bookDao();
​
    public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            database.execSQL("create table Book (id integer primary key autoincrement not null, name text not null, pages integer not null)");
        }
    };
    public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
        //参数表示数据库从版本2升级到3
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            database.execSQL("alter table Book add colum author not null default 'unknown'");
        }
    };
​
    private static AppDatabase instance = null;
​
    public static AppDatabase getInstance(Context context) {
        if (instance == null) {
            Builder<AppDatabase> database = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "database")
                    .addMigrations(MIGRATION_1_2, MIGRATION_2_3);
            //将MIGRATION_1_2, MIGRATION_2_3作为参数同时传递
            
            instance = database.build();
        }
        return instance;
    }
​
}

注解@Database的版本号变成了3;然后编写一个Migration类型的变量,重写方法migrate()中写alter语句添加一列。最后添加到addMigration()方法的参数中。

WorkManager

WorkManager适合用于处理一些要求定时执行的任务,它可以根据操作系统的版本自动选择底层是使用AlarmManager还是JobScheduler实现,从而降低了我们的使用成本。另外WorkManager还支持周期性任务、链式任务处理等功能,是一个非常强大的工具。

WorkManager和Service并不相同,也没有直接的联系。WorkManager只是一个处理定时任务的工具,它可以保证即使在应用退出甚至手机重启的情况下,之前注册的任务仍然会得到执行。因此WorkManager很适合用于执行一些定期和服务器进行交互的任务。

WorkManager的基本用法

导依赖包:

implementation 'androidx.work:work-runtime:2.8.1'

WorkManager的用法主要分为以下三步:

  1. 定义一个后台任务,并实现具体的逻辑;

  2. 配置该后台任务的运行条件和约束信息,并构建后台任务请求;

  3. 将该后台任务请求传入WorkManager的enqueue()方法中,系统会在合适的时间运行。

首先定义一个后台任务

新建一个SimpleWorker类:

public class SimpleWorker extends Worker {
​
    
    public SimpleWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }
​
    @NonNull
    @Override
    public Result doWork() {
        Log.d("TAG", "doWork: in SimpleWorker");
        return Result.success();
    }
}

每一个后台任务都必须继承自Worker类,并调用它唯一的构造函数。然后重写父类的doWork()方法,在这个方法中编写具体的后台任务逻辑即可。

doWork()方法不会运行在主线程当中,可以放心的在这里执行耗时逻辑。

doWork()方法会返回一个Result对象,用于表示任务的运行结果,成功就返回Result.success(),失败就返回Result.failure()。还有Result.retry()方法,他也表示失败,可以结合WorkRequest.Builder的setBackoffCriteria()方法来重新执行任务。

配置该后台任务的运行条件和约束信息,并构建后台任务请求

WorkRequest.Builder的子类:

  • OneTimeWorkRequest.Builder 用于构建单次运行的后台任务请求。

    OneTimeWorkRequest request1 = new OneTimeWorkRequest.Builder(SimpleWorker.class).build();
    //传递的参数是刚才创建的 后台任务的class对象

  • PeriodicWorkRequest.Builder 可用于构建周期性运行的后台任务请求。但是为了降低设备性能消耗,该够早函数中传入的运行周期间隔不能短于15 分钟。

    PeriodicWorkRequest request2 = new PeriodicWorkRequest.Builder(SimpleWorker.class, 15, TimeUnit.MINUTES).build();
    //参数:1.后台任务类的class对象;2.运行周期间隔时间;3.时间单位

将后台任务请求传入WorkManager的enqueue()方法中

创建出后台任务请求对象之后,传入WorkManager的enqueue()方法中,系统就会在合适的时间去执行了:

WorkManager.getInstance(this).enqueue(request1);

代码示例:

<Button
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/doWorkBtn"
    android:text="Do Work"/>
//在activity_main.xml文件中添加了一个按钮,点击开始执行任务
​
...
findViewById(R.id.doWorkBtn).setOnClickListener(view ->{
    OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(SimpleWorker.class).build();
    WorkManager.getInstance(this).enqueue(request);
    //在MainActivity中的onCreate()方法中设置按钮的监听事件
});

使用WorkManager处理复杂的任务

让后台任务在指定的延迟时间后运行

借助setInitialDelay()方法。

OneTimeWorkRequest request1 = new OneTimeWorkRequest.Builder(SimpleWorker.class)
    .setInitialDelay(5, TimeUnit.MINUTES) //5, 单位分钟
    .build();

表示让SimpleWorker这个给后台任务在5分钟之后运行。

通过标签来取消后台任务请求

如果没有标签可以通过id来取消后台任务请求:

WorkManager.getInstance(this).cancelWorkById(request.getId());

使用id只能取消单个后台任务请求,而使用标签可以将头一标签的后台任务请求全部取消。

WorkManager.getInstance(this).cancelAllWorkByTag("TAG");

这种取消针对的是将“TAG”设置为标签的后台任务请求:(addTag()方法)

OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(SimpleWorker.class)
        .addTag("Tag")
        .setInitialDelay(5, TimeUnit.MINUTES)
        .build();

一次清空所有后台任务请求:

WorkManager.getInstance(this).cancelAllWork();

后台任务失败后重新执行

如果后台任务的doWork()方法中失败返回了Result.retry(),此时是可以结合setBackoffCriteria()方法来重新执行任务:

OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(SimpleWorker.class)
        .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
        .build();

setBackoffCriteria()方法接受三个参数:

第一个参数用于指定如果任务再次执行失败,下次重试的时间应该以什么样的形式延迟。有两个可选值:LINEAR代表下次重试的时间已线性的方式延迟;EPONENTIAL表示下次重试的时间以指数的方式延迟。

第二、第三个参数用于指定在多久后第一次重新执行任务,时间最短不能少于10秒。

监听后台任务执行结果(doWork()方法的返回状态)

//对doWork()方法的返回结果(后台任务运行结果)进行监听
WorkManager.getInstance(this)
        .getWorkInfoByIdLiveData(request.getId())
        .observe(this, new Observer<WorkInfo>() {
            @Override
            public void onChanged(WorkInfo workInfo) {
                if (workInfo.getState() == WorkInfo.State.SUCCEEDED){
                    //当doWork()方法返回的是Result.success();
                    Log.d("TAG", "onChanged: 后台任务执行成功");
                } else if (workInfo.getState() == WorkInfo.State.FAILED){
                    //当doWork()方法返回的是Result.failure();
                    Log.d("TAG", "onChanged: 后台任务执行失败");
                }
            }
        });

调用WorkManager实例的getWorkInfoByIdLiveData()方法,参数传入后台任务请求的id。这个方法会返回一个LiveData对象。可以直接调用LiveData对象的observe()方法来观察结果。

另外也可以调用getWorkInfosByTagLiveData()方法监听同意标签名下所有的后台任务请求的运行结果,参数需要传入一个"Tag"。

链式任务

OneTimeWorkRequest a = new OneTimeWorkRequest.Builder(SimpleWorker.class).build();
OneTimeWorkRequest b = new OneTimeWorkRequest.Builder(SimpleWorker.class).build();
OneTimeWorkRequest c = new OneTimeWorkRequest.Builder(SimpleWorker.class).build();
WorkManager.getInstance(this)
        .beginWith(a)
        .then(b)
        .then(c)
        .enqueue();

现在有a、b、c三个独立的后台任务。要求先执行a、在执行b、最后执行c的功能,就可以借助链式任务来实现。

调用WorkManager的实例方法beginWith()表示开启一个链式任务,之后需要用then()方法连接,最后调用enqueue()方法递交任务请求。

对于链式任务,只有前一个任务执行成功之后,下一个任务才会执行。

WorkManager可以用,但别依赖他去实现什么核心和功能,因为他在国产手机上可能不太稳定。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值