android12——Jetpack

Jetpack

Jetpack 是一套库、工具和指南,可帮助开发者更轻松地编写优质应用。这些组件可帮助您遵循最佳做法、让您摆脱编写样板代码的工作并简化复杂任务,以便您将精力集中放在所需的代码上。

Jetpack中的组件有一共特点,它们大部分依赖任何android系统版本,这意味着这些组件通常是定义在androidX库当中的,并且拥有非常好的向下兼容性。另外jetpack中的许多构架组件是专门为MVVM架构量身打造的。

ViewModel

在传统的开发模式下,activity的任务实在是太重了,既要负责逻辑处理,又要控制UI展示,甚至还要处理网络回调等等。而 ViewModel作为Jetpack最重要的组件之一,他可以帮助activity分担一部分工作,他是专门用于存放和界面相关的数据的。也就是说,只要是界面上能看得到的数据,它的相关变量都应该存放在ViewModel中,而不是activity中,这样可以在一定程度上减少activity中的逻辑。

另外,viewmodel还有一个非常重要的特性。我们都知道,当手机发生横竖屏旋转的时候,activity会被重新粗行间,同时存放在activity中的数据也会丢失。而viewmodel的声明周期和activity不同,他可以保证在手机屏幕发生旋转的时候不会被重新创建,只有当activity退出的时候才会跟着activity一起销毁。因此,将与界面相关的变量存放在viewmode当中,这样即使旋转手机屏幕,界面上的数据也不会丢失。viewmodel的生命周期如下图所示:

ViewModel的基本用法

  1. 创建JetpackTest项目

  2. jetpack的组件通常是以androidX库的形式发布的,但是viewmodel组织还是需要添加依赖:

    implementation 'androidx.lifecycle:liftcycle:lifecycle-extenions:2.1.0'
    
  3. 按照编程规范给每一个activity和fragment都创建一个对应的ViewModel,MainActivity=>MainViewModel,并集成viewmodel。添加计数器变量counter

    import androidx.lifecycle.ViewModel;
    
    public class MainViewModel extends ViewModel {
        private int counter = 0;
    }
    
  4. 在layout上添加计数器。(activity_main.xml

        <TextView
            android:id="@+id/info_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:textSize="32sp"/>
        <Button
            android:id="@+id/plus_one_btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="Plus One"/>
    
  5. MainActiviy中实现计数器逻辑:

    注意:这个地方不可以是直接去创建ViewModel实例,而是通过Provider获取的,因为 ViewModel有独立的生命周期,并且其生命周期要长于Activiy。如果我们在onCreate()中创建ViewModel的实例,那么每次执行onCreate()都会创建一个新的实例,这样当手机旋转的时候,就无法保留其中的数据了。

    另外,即使旋转,数据也不会丢失!

    public class MainActivity extends AppCompatActivity {
        private TextView infoText;
        private Button plusOneBtn;
        private MainViewModel mainViewModel;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            infoText = findViewById(R.id.info_text);
            plusOneBtn = findViewById(R.id.plus_one_btn);
            // 下面这个方法过时了:https://blog.csdn.net/sinat_33150417/article/details/104323897
    //        MainViewModel mainViewModel = ViewModelProviders.of(this).get(MainViewModel.class);
            mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
            plusOneBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mainViewModel.counter++; // public 但是不符合开闭原则
                    refreshCounter();
                }
            });
            refreshCounter();
        }
    
        private void refreshCounter() {
            // 注意setText必须是String类型不能是int,不然会报错
            infoText.setText(String.valueOf(mainViewModel.counter));
        }
    }
    

向ViewModel传递参数

上一节创建的MainViewModel的构造函数中没有任何参数,如果我们确实需要通过苟傲函数来传递一些参数的时候,则需要借助ViewModelProvider.Factory就可以实现了。

创建一个factory类:

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

在创建ViewModel的时候增加factory的参数:

mainViewModel = new ViewModelProvider(this, new MainViewFactory(count_reserved)).get(MainViewModel.class);

关于持久化计数器:

    private static final String TAG = "MainActivity";
    private TextView infoText;
    private Button plusOneBtn;
    private Button clearBtn;
    private MainViewModel mainViewModel;
    private SharedPreferences sp;
    private SharedPreferences.Editor editor;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        infoText = findViewById(R.id.info_text);
        plusOneBtn = findViewById(R.id.plus_one_btn);
        clearBtn = findViewById(R.id.clear_btn);
//        sp = PreferenceManager.getDefaultSharedPreferences(this); // getPreferences(Context.MODE_PRIVATE)的区别?
        sp = getSharedPreferences("counter",MODE_PRIVATE);
        editor =  sp.edit();
        int count_reserved = sp.getInt("count_reserved", 0);
        // 下面这个方法过时了:https://blog.csdn.net/sinat_33150417/article/details/104323897
//        MainViewModel mainViewModel = ViewModelProviders.of(this).get(MainViewModel.class);
        mainViewModel = new ViewModelProvider(this,new MainViewFactory(count_reserved)).get(MainViewModel.class);


        plusOneBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mainViewModel.counter++; // public 但是不符合开闭原则
                refreshCounter();
            }
        });

        clearBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mainViewModel.counter = 0;
                refreshCounter();
            }
        });
        refreshCounter();
    }

    private void refreshCounter() {
        // 注意setText必须是String类型不能是int,不然会报错
        infoText.setText(String.valueOf(mainViewModel.counter));
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG, "onPause: counter:" + mainViewModel.counter);
        editor.putInt( "count_reserved", mainViewModel.counter);

        if(editor.commit()){// 和apply的区别是有返回值 如果不commit是进不去的!!!
            Log.d(TAG, "onPause: count_reserved: "+ String.valueOf(sp.getInt("count_reserved",-1)));
        }else {
            Log.d(TAG, "onPause: Error commit");
        }
    }

Lifecycles

在编写android应用层序的时候,可能会经常遇到需要感知activity生命周期的情况,以便在适当的时候进行相应的逻辑控制。

在一个activity中去感知它的生命周期非常简单,而如果要在一个非activity的类中去感知activity的生命周期就可能需要借助监听器等方式来完成。但是这种方式就需要自己编写大量的逻辑代码。而Lifecycles组件可以让任何一个类都能够轻松感知到activity的生命周期,同时又不需要在activity中编写太多额外的逻辑。

Observer的用法非常简单: 实现接口,并使用注解:

import android.util.Log;

import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;

public class MyObserver implements LifecycleObserver {
    private static final String TAG = "MyObserver";
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    public void activityStart(){
        Log.d(TAG, "activityStart");
    }
}

借助LifecycleOwner,在生命周期发生变化的时候通知MyObserver,我们可以实现一个LifecycleOwner接口的实现类。首先调用LifecycleOwner的getLifeOwner的getLifecycle()方法,得到一个Lifecycle对象,然后调用它的addObserver()方法来观察LifecycleOwner的生命周期,再把MyObserver的实例传进去就可以了。

那么LifecycleOwner是什么呢?如何才能获取一个LifecycleOwner的实例。其实没有必要去自己实现一个LifecycleOwner,因为只要你的Activity是继承自AppCompatActivity的,或者你的Frgment是继承自androidx.fragment.app.Fragment的,那么他妈本身就是一个LifecycleOwner的实例。

所以在MainActiviy中的代码可以这么写:

Lifecycle lifecycle = getLifecycle();
lifecycle.addObserver(new MyObserver());

lifecycle.currentState返回的生命周期状态是一个枚举类型,一共有INITALIZED、DESTORYED、CREATED、STARTED、RESUMED这5种状态,他们与activity的生命周期回调所对应的关系如下图:

LiveData

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

LiveData的基本用法

我们一直使用的都是在activity中手动获取viewmodel中的数据这种交互方式,但是viewmodel却无法将数据的变化主动通知给activity。虽然,把activity的实例传给viewmodel,这样viewmodel就能主动对activity进行通知。但是,由于viewmodel的声明周期是长于activity的,如果吧activity的实例传递给viewmodel,就很有可能会因为activity无法释放而造成内存泄漏,这是一种非常错误的做法。

而LiveData可以包含任何类型的数据,并在数据发生变化的时候通知给观察者。也就是说,如果我们将计数器使用livedata来包装,然后在activity中去观察他,就可以主动将数据变化通知给activity了。

我们可以修改MainViewModel的代码,具体如下:

public class MainViewModel extends ViewModel {
    private MutableLiveData<Integer> counter;

    public MainViewModel(){
        counter = new MutableLiveData<>();
        counter.setValue(0);
    }
    public MainViewModel(int counter){
        this.counter = new MutableLiveData<>();
        this.counter.setValue(counter);
    }
    public void plusOne(){
        counter.setValue((counter.getValue() == null ? 0 : counter.getValue()) + 1);
    }

    public void clear(){
        counter.setValue(0);
    }

    public int getCounter(){
       return  this.counter.getValue() == null ? 0 : this.counter.getValue();
    }
}
}

这里我们将counter变量修改成了MutableLiveData对象,并指定他的泛型为Integer。MutableLiveData是一种可变的LiveData,用法很简单,主要有3种读写数据的方法,分别是getValue()setValue()postValue()方法,用于获取数据,设置数据(但是只能在主线程中调用),在非主线程线程中给LiveData设置数据。

最后,上面还有有点不够规范,应该只暴露不可变的LiveData给外部。这样在非ViewModel中就稚嫩观察LiveData的数据变化,而不能给LiveData设置数据。因此改造如下:

public class MainViewModel extends ViewModel {
    private final MutableLiveData<Integer> counter;

    public MainViewModel(){
        counter = new MutableLiveData<>();
        counter.setValue(0);
    }
    public MainViewModel(int counter){
        this.counter = new MutableLiveData<>();
        this.counter.setValue(counter);
    }

    public void plusOne(){
        counter.setValue((counter.getValue() == null ? 0 : counter.getValue()) + 1);
    }

    /**
     * 最终返回的时候是一个LiveData<Integer> 他是不可变的
     */
    public LiveData<Integer> getCounter(){
        return counter;
    }
}

map和switchMap

map

LiveData的基本用法可以满足大部分的开发需求,但是当项目变得复杂之后,可能会出现一些更加特殊的需求,比如一个POJO类User,如果我们只关心用户的姓名,而实际上将整个User类型的LiveData暴露给外部就不合适了。整个时候就可以使用map()方法,他可以将User类型的LiveData自由地转型成任意其他类型的LiveData。具体代码如下:

public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class MainViewModel extends ViewModel {
    private final MutableLiveData<Integer> counter;
    private final MutableLiveData<User> userLiveData = new MutableLiveData<>();
    LiveData<String> userName = Transformations.map(userLiveData, User::getName);
}

上面我们调用了Transformations的map()方法来对LiveData的数据类型进行转换。第一个参数是原始的LiveData对象,第二个参数是转换函数,逻辑就是将User对象转换成一个只包含用户姓名的字符串。

switchMap

他的使用场景非常固定,但是可能比map方法要更加常用。上面的所有的内容都有一个前提:LiveData对象的实例都是在ViewModel中创建的。而实际的项目中,不可能一直是这种情况,很可能是ViewModel中的某个LiveData对象是调用另外的方法获取的。

假设存在一个单例类Repository,代码如下:

public class Repository {
    public static LiveData<User> getUser(String userId){
        MutableLiveData<User> liveData = new MutableLiveData<>();
        liveData.setValue(new User(userId,"ssozh",0));
        return liveData;
    }
}

这里我们在Repository类中添加了一个getUser()方法,这个方法接受一个userId参数。我们在MainViewModel中也是获取LiveData对象。

    public LiveData<User> getUser(String userId){
        return Repository.getUser(userId);
    }

接下来的问题是,在activity中如何观察LiveData的数据变化呢?

  • 首先通过写一个getUser()方法来调用肯定是错误的,因为你这样调用观察的是捞的LiveData实例,根本无法观察到数据的变化。
  • 使用switchMap()方法【下面代码是对着kotlin写的可能有问题】
    private MutableLiveData<String> userIdLiveData = new MutableLiveData<>();
    LiveData<User> user = Transformations.switchMap(userIdLiveData,userId ->{
        return Repository.getUser(userId);
    });
    public LiveData<User> getUser(String userId){
        return Repository.getUser(userId);
    }

switchMap()方法同样接受两个参数:

  • 第一个参数用来传入我们新增的userIdLiveData,switchMap()方法对他进行观察。
  • 第二个参数是一个转换函数,我们必须在这个转换函数中返回一个LiveData对象,因为switchMap()方法的工作原理就是要将转换函数中返回的LiveData对象转换成另一个可观察的LiveData对象。

switchMap()的整体工作流程:当外部调用MainViewModel的getUser()方法来获取用户数据时,并不会发起任何请求或者函数调用,只会将传入的userId值设置到userIdLiveData当中。一旦uerIdLiveData的数据发生变化,那么观察userIdLiveDataswitchMap()方法就会执行。并且调用我们编写的转换函数,然后在转换函数中调用Repository.getUser()方法返回的LiveData对象转换成一个可观察的LiveData对象,对于activity而言,只要去观察这个LiveData对象就可以了。

Room【概述】

Room是android官方推出的一个ORM框架,并将他加入到了jetpack当中。

使用Room进行增删改查

Room是由Entity、Dao和Database这三个部分组成,每个部分都有明确的职责,具体说明如下:

  • Entity:用于定义封装实际数据的实体类,每个实体类都会子啊数据库中有一张丢应的表,并且表中的列是根据实体类中的字段自动生成的。
  • Dao:Dao是数据访问对象的意思,通常会在这里对数据库的各项操作进行封装,在实际编程的时候,逻辑层就不需要和底层数据库打交道了,直接和Dao层打交道交互即可。
  • Database:用于定义数据库中的关键信息,包括数据库的版本号,包含哪些实体类以及提供的Dao层访问实例。

‘使用:

  • 添加依赖
  • 定义实体类Entity:添加@Entity注解
  • 定义Dao【最关键部分】:
    • 创建一个UserDao接口,并添加@Dao注解。
    • 接口中的方法添加@Insert等注解。
  • 定义Database【写法固定】:
    • 其包含三部分内容:数据库的版本号,包含哪些实体类,Dao层的访问实例。
    • 添加@Database注解,继承RoomDatabase类,并且声明成抽象类。【这个地方只需要声明就行,具体的方法是由Room在底层自动完成的】
    • kotlin在companion object结构体中编写一个单例模式。【Java就是静态方法】
  • 测试【略】

Room的数据库升级【略】

Room在数据库升级方面设计得非常繁琐,基本上没有比SQLiteDatabase简单到哪去,每次升级都需要手动编写升级逻辑才行。

WorkManager

android的后台机制是一个很复杂的话题。在很早之前,android系统的后台系统是非常开发的,service的优先级也很高,仅次于activity,那个时候可以在service中做很多事情。但是由于后台功能太过于开放,每个应用都想无限地占用后台资源,导致手机的内存越来越紧张,好点越来越快,也变得越来越卡。为了解决这些情况,基本上android系统每发布一个新版本,后台权限都会被进一步收紧。

从4.4系统的AlarmManager开始到5.0的JobScheduler来处理后台任务,再到6.0的Doze和App Standby模式,再到8.0直接禁用了后台功能只允许使用前台service。一直在修改与后台相关的API。这么频繁的功能和API变更,让开发者更难受了,为了解决这个问题,google退出了workmanager组件。

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

不过,WorkManager和Service并不相同,也没有直接的联系。Service是android系统的四大组件之一,它在没有被销毁的情况下是一直保持在后台运行的。而WorkManager只是一个处理定时任务的工具,它可以保证即使在应用退出甚至手机重启的情况下,之前注册的任务仍然将得到执行,因此workmanager很适合用于执行一些定期和服务器进行交互的任务,比如周期性地同步数据等等。

WorkManager的基本用法

基本用法:

  • 添加依赖
  • 定义一个后台任务,并实现具体的任务逻辑
  • 配置该后台任务的运行条件和约束信息,并构建后台任务请求;
  • 将该后台任务请求传入WorkManager的enqueue()方法中,系统会在合适的时间运行。

添加依赖

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

定义后台任务

后台任务的写法非常固定:

  • 首先每个后台任务必须继承自Worker类,并调用它唯一的构造函数。
  • 然后重写父类的doWork()这个方法,在这个方法中编写具体id后台任务逻辑即可。
    • doWork()方法不会运行在主线程中,因此你可以放心的在这里执行耗时逻辑。
    • doWork()方法要求返回一个Result对象,用于表示任务的运行结果。
    • 还有一个Result.retry()方法,他代失败,只是可以结合WorkRequest.Buidler的setBackoffCriteria()方法来重新执行任务。
// 创建一个SimpleWorker类
import android.content.Context;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

public class SimpleWorker extends Worker{
    private static final String TAG = "SimpleWork";

    public SimpleWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @NonNull
    @Override
    public Result doWork() {
        Log.d(TAG, "doWork: do work in SimpleWorker");
        return Result.success();
    }
}

配置该后台任务的运行条件和约束信息

在MainActivity中,把上面创建的后台任务的Class对象传入OneTimeWorkRequest.Builder的构造函数中,然后调用build()方法即可完成构建。

  • OneTimeWorkRequest.Builder是WorkRequest.Builder的子类,用于构建单词运行的后台任务请求。
  • PeriodicWorkRequest.Builder也是WorkRequest.Builder的子类,可用于构建周期性运行的后台任务,但是为了降低设备性能消耗,PeriodicWorkRequest.Buidler构建函数中传递的运行周期间隔不能低于15分钟。
// WorkManager的基本用法
// OneTimeWorkRequest.Builder是WorkRequest.Builder的子类,用于构建单词运行的后台任务请求。
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(SimpleWorker.class).build();
//  PeriodicWorkRequest.Builder也是WorkRequest.Builder的子类,可用于构建周期性运行的后台任务
PeriodicWorkRequest request1 = new PeriodicWorkRequest.Builder(SimpleWorker.class, 15, TimeUnit.MINUTES).build();



Button doWorkBtn = findViewById(R.id.do_work_btn);
doWorkBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Operation enqueue = WorkManager.getInstance(MainActivity.this).enqueue(request);
        //        WorkManager.getInstance(this).enqueue(request1);
    }
});

使用WorkManager处理复杂的任务

虽然成功运行了一个后台任务,但是由于不能控制它的具体运行时间,因此并没有什么太大的实际用处。实际上WorkManager还允许我们控制许多其他方面的东西,包括:

  • 让后台任务再指定的延时时间后运行 => 借助etInitilaDelay()方法

  • 给后台任务请求添加标签 => OneTimeWorkRequest.Builder()后面 addTag()

    • 可以通过标签来取消后台任务请求,当然也可以通过request.id来取消。
  • 一次性取消所有后台任务请求 => WorkManager.getInstance(Context).cancelAllWork()

  • Result.retry()结合setBackoffCriteria()方法来重新执行任务 => WorkManager.getInstance(Context)后面 setBackoffCriteria(),其中接受三个参数:

    • 第一个参数用于指定如果任务再次执行失败,下次重试的时间应该以什么样的形式延迟(BackoffPolicy)。有两个策略,分别是线性和指数。
    • 第二个参数(数字)和第三个参数(单位)用于指定在多久之后重新执行任务,不能短于10分钟
  • Result.success和Result.failure的监听任务:

    WorkManager.getInstance(MainActivity.this).getWorkInfoByIdLiveData(request1.getId())
        .observe(MainActivity.this, workInfo -> {
            if(workInfo.getState() == WorkInfo.State.SUCCEEDED)
                Log.d(TAG, "onClick: do work succeeded");
        });
    

    其中getWorkInfoByIdLiveData传入后台请求任务的id,会返回一个LiveData对象,然后就可以调用LiveData对象的observe()方法来观察数据变化了。

  • 链式任务:

    Operation enqueue = WorkManager.getInstance(MainActivity.this)
        .beginWith(work1)
        .then(work2)
        .then(work3)
        .enqueue();
    

    注意,一旦某个任务运行失败了,或者被取消了,那么接下来的后台任务就都得不到运行了。

posted @ 2020-12-27 15:27  SsoZh  阅读(120)  评论(0编辑  收藏  举报