zoukankan      html  css  js  c++  java
  • 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();
      

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

  • 相关阅读:
    java 多线程 继承Thread和实现Runnable的区别
    TCP和UDP的区别
    重载与覆盖(java)
    感悟
    ant design + react带有二级导航菜单自动生成
    自己搭建ant design框架
    ant design框架学习
    radio美化
    nodejs-微信公众号 ----答疑机器人
    微信小程序----开发小技巧(二)
  • 原文地址:https://www.cnblogs.com/SsoZhNO-1/p/14197424.html
Copyright © 2011-2022 走看看