JetPack学习笔记

170 阅读14分钟

以下是关于JetPack学习的一些笔记

目录:

  • ContraintLayout
  • ViewModel
  • LiveData
  • DataBinding
  • Navigation
  • LifeCycle
  • Room
  • BottomNavitation
  • Paging
  • Volley
  • Glide
  • Swiperefreshlayout

1.ContraintLayout

implementation 'androidx.constraintlayout:constraintlayout:2.0.1'

这里首先学到了魔法棒可以自动建立链接,控件的Attributes中可以点击右侧按钮直接编辑资源文件。 ComponentTree中可以展示控件的结构和问题点,最好按规范逐一解决。 Ctrl+左键可以删除约束 工具栏有用的工具 pack 将多个组件打包 Expand 将多个组件拉伸 Distribute 在指定方向上将多个组件连起来 align 对齐,这里类似ppt里面的操作,可以横向纵向分布,按指定边对其。这里有种BaseLine对其,就是按控件文字下边进行对其。 guideline 辅助线,可以定义横向或者纵向的辅助线,为其指定百分比,再让该子组件根据辅助线定位。 Barrier 是看不见的,像一个容器一样需要放置多个控件在其内部(在Comment Tree中拖动即可) Group 可以定义一个分组,统一管理内部的子组件,为其设置同样的属性配置

2.ViewModel

首先明确ViewModel是做什么的?它用于在Activity因配置变更销毁重建的时候保存View中的数据。 因为 onSaveInstanceState() 和 onCreate()只能传递少量数据。如下是一个文本框和一个按钮组成的累加器,这里通常情况下saveInstanceState是为null的,只有异常退出的时候才会有值,在onSaveInstanceState方法中存入少量数据。这些数据维护在activity中。activity不仅管理数据还得处理视图的更新和赋值,逻辑多了之后会很庞大。

    private int result;
    private String RESULT_KEY;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (savedInstanceState != null) {
            result = savedInstanceState.getInt(RESULT_KEY);
        }
        mResultTv = findViewById(R.id.tv_result);
        mResultTv.setText(String.valueOf(result));
        mAddOneBtn = findViewById(R.id.button);
        mAddOneBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mResultTv.setText(String.valueOf(++result));
            }
        });
    }

    @Override
    protected void onSaveInstanceState(@NonNull Bundle outState) {
        outState.putInt(RESULT_KEY, result);
        super.onSaveInstanceState(outState);
    }

一个ViewModel实例只有在Activity主动调用finish()的时候才会被销毁。ViewModel是一个实例对象,可以持有View需要的数据。ViewModel的存活周期和Activity一样,但不受因配置变更导致的Activity销毁和重建。 (1)先引入依赖,隶属于lifecycle这个包下

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

(2)继承系统的ViewModel类,暂存自己的数据。

public class MainActivityViewModel extends ViewModel {
    private int result;

    public int getResult() {
        return result;
    }

    public void setResult(int result) {
        this.result = result;
    }
}

(3)在Activity中实例化ViewModel并建立绑定关系。 只需在Activity中创建一个成员变量,在onCreate时对其赋值,使用provider来创建。

mMainActivityViewModel = new ViewModelProvider(this).get(MainActivityViewModel.class);

(4)在需要使用和变更数据的地方使用ViewModel的get/set方法。

mAddOneBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        mMainActivityViewModel.setResult(mMainActivityViewModel.getResult()+1);
        mResultTv.setText(String.valueOf(mMainActivityViewModel.getResult()));
    }
});

这样在因配置变更导致的Activity重新创建后,暂存的数据不会丢失。ViewModel中暂存的数据也没有过多限制。省去了在onSaveInstanceState的处理逻辑。Activity中也不必处理数据的逻辑,更好的解耦。

lifecycle-viewmodel-savedstate

上面的ViewModel可以在配置变更的时候保存数据,但如果Activity处于栈底由于系统优先级低导致被回收,则数据也会被回收。如果你想在内存中数据保存的更旧,则可以使用lifecycle-viewmodel-savedstate这个扩展来实现。 (1)引入依赖

    // Saved state module for ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"

(2)在ViewModel中使用SaveStateHandle 之前是需要在ViewModel中定义成员变量来保存数据,针对需要在应用内存中一直保存的数据,这里只需要定义KEY,然后在获取数据的时候使用mSavedStateHandle.getLiveData(KEY_RESULT) 来获取数据。

public class MainActivityViewModel extends ViewModel {
    public static final String KEY_RESULT = "KEY_RESULT";
    private SavedStateHandle mSavedStateHandle;

    public MainActivityViewModel(SavedStateHandle savedStateHandle) {
        mSavedStateHandle = savedStateHandle;
    }

    public MutableLiveData<Integer> getLiveResult() {
        if (!mSavedStateHandle.contains(KEY_RESULT)){
            mSavedStateHandle.set(KEY_RESULT,0);
        }
        return mSavedStateHandle.getLiveData(KEY_RESULT);
    }
    // 业务函数
    public void add() {
        getLiveResult().setValue(getLiveResult().getValue()+1);
    }
}

(3)在Activity中创建ViewModel 这里需要增加一个SavedStateViewModelFactory实例。

// 创建ViewModel
viewModel = new ViewModelProvider(this,new SavedStateViewModelFactory(this.getApplication(),this)).get(MainActivityViewModel.class);

通过上述方式就可以保证在Activity在被后台清掉之后,我们缓存的数据还是存在的,因为这里是的SavedStateHandle是和application的生命周期一样长的。 这里再补充下SavedStateViewModelFactory的第二个参数实际上需要个SavedStateRegistryOwner类型的参数,这里activity实现了该接口,所以就使用了this。

注意:这里可以在开发者选项中打开 禁止保留Activities 和 后台进程设为no background progress

2.LiveData

上面的代码中我们使用ViewModel来管理了数据,但数据变更后还是要更新界面,一般是通过回调来完成的。在上面的点击事件中我们先将result+1之后存入ViewModel再来对ResultTv重新赋值,如果有多个地方都会导致数据变更,就需要再多个回调中先保存数据到ViewModel再更新TextView,这样产生了很多重复代码。 ResultTv的职责很明确,就是显示result的值,不关心业务上是怎么变更result的。所以这里使用观察者模式来优化这个步骤,让ResultTv来订阅result的状态,当有变化时自动更新。 LiveData是一个容器类,可以装指定类型的数据。 LiveData会在底层数据变更的时候通知视图。它可以感知生命周期变化,只通知更新那些活跃的视图。 LiveData是一个抽象类通常我们使用其实现类MutableLiveData,当然也可以自己实现进行扩展。 (1)引入依赖

def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"

(2)在ViewModel中创建LiveData实例

private MutableLiveData<Integer> liveResult;
public MutableLiveData<Integer> getLiveResult() {
    if (liveResult==null){
        liveResult = new MutableLiveData<>();
    }
    return liveResult;
}
public void setLiveResult(MutableLiveData<Integer> liveResult) {
    this.liveResult = liveResult;
}

(3)在Activity中创建观察者,处理订阅事件,将观察者和LiveData绑定,初始化数据。

final Observer<Integer> resultObserver = new Observer<Integer>() {
    @Override
    public void onChanged(Integer integer) {
        mResultTv.setText(String.valueOf(integer));
    }
};
mMainActivityViewModel.getLiveResult().observe(this,resultObserver);
mMainActivityViewModel.getLiveResult().setValue(0);

(4)通过setValue(只能在主线程)或者postValue(可以在子线程)来修改数据。

mAddOneBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        mMainActivityViewModel.getLiveResult().setValue(mMainActivityViewModel.getLiveResult().getValue() + 1);
    }
});

以上就是LiveData的基本使用,只要数据通过setValue或者postValue修改过则会通知订阅者更新UI。 注意:ViewModel一定不能持有视图层的引用,同样不能持有Context的引用!不然还是MVP 打脸了ViewModel是可以持有应用Context的,是不可以持有Activity,甚至官方也提供了在ViewModel中获取Context的方式。 AndroidViewModel 让自定义的ViewModel继承AndroidViewModel就可以通过getApplication()即可获取到Application实例。这为我们在ViewModel中通过Context来获取资源提供了方便。在Activity中实例化View Model的方式和之前实一样的。

public class MainActivityViewModel extends AndroidViewModel {
    private SavedStateHandle mSavedStateHandle;
    String key = getApplication().getResources().getString(R.string.data_key);
    String shpName = getApplication().getResources().getString(R.string.shp_name);

    public MainActivityViewModel(@NonNull Application application,SavedStateHandle savedStateHandle) {
        super(application);
        this.mSavedStateHandle = savedStateHandle;
        if (!mSavedStateHandle.contains(key)){
            load();
        }
    }
    // 如果内存中没有数据则从本地SP文件加载
    public void load(){
        SharedPreferences shp = getApplication().getSharedPreferences(shpName,Context.MODE_PRIVATE);
        int x= shp.getInt(key,0);
        mSavedStateHandle.set(key,x);
    }
    // 将数据持久化到我们指定的SP文件,可以在点击时保存,也可以在onPause时调用
    public void save(){
        SharedPreferences shp = getApplication().getSharedPreferences(shpName,Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = shp.edit();
        editor.putInt(key,getLiveResult().getValue());
        editor.apply();
    }
    
    public MutableLiveData<Integer> getLiveResult() {
        return mSavedStateHandle.getLiveData(key);
    }
    // 业务函数
    public void add() {
        getLiveResult().setValue(getLiveResult().getValue()+1);
    }
}
// 创建ViewModel
viewModel = new ViewModelProvider(this,
								  new SavedStateViewModelFactory(
								      this.getApplication(),
								  	  this
								  )
		    ).get(MainActivityViewModel.class);

3.DataBinding

以声明的形式将可观察的数据绑定到界面元素,进一步将控制器精简。 通过DataBinding这个中间件建立控制器和View(XML)之间的联系。使得Activity和View之间进一步解耦。模块化的分离。下面就看下DataBinding是如何解耦的,

private MainActivityViewModel mMainActivityViewModel;
private TextView mResultTv;
private Button mAddOneBtn;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mMainActivityViewModel = new ViewModelProvider(this).get(MainActivityViewModel.class);
    mResultTv = findViewById(R.id.tv_result);
    mResultTv.setText(String.valueOf(mMainActivityViewModel.getResult()));
    final Observer<Integer> resultObserver = new Observer<Integer>() {
        @Override
        public void onChanged(Integer integer) {
            mResultTv.setText(String.valueOf(integer));
        }
    };
    mMainActivityViewModel.getLiveResult().observe(this, resultObserver);
    mAddOneBtn = findViewById(R.id.button);
    mAddOneBtn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            mMainActivityViewModel.getLiveResult().setValue(mMainActivityViewModel.getLiveResult().getValue() + 1);
        }
    });
}

上面的代码是使用ViewModel和LiveData优化过的累加器,这里只有两个控件,如果界面比较复杂就会有很多控件成员变量、findViewById、为控件赋值取值的代码、

(1)在构建文件android插件下开启databinding

android {
	....
    dataBinding {
        enabled = true
    }
    // gradle5.0 之后需要这样启用
 	buildFeatures{
        dataBinding true
    }
    ....
}

(2)修改布局文件 这里修改两个地方,第一是在最外层使用layout标签包裹,第二是添加一个data标签在其内部使用variable添加当View中需要绑定的数据源,这里我们只添加定义的ViewModel即可。 注意:这里有个小技巧,光标移动到最外层布局容器上,按Alt+Enter会弹出Convert to data binding layout的选项,可以直接完成上述修改。

<layout 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" >
    该View绑定的数据集合
    <data>
        <variable
            name="viewModel"
            type="com.example.viewmodeldemo.MainActivityViewModel" />
    </data>
	页面布局部分
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    	......   
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

(3)在Activity中建立和视图的绑定关系 这里的ActivityMainBinding类型是根据布局文件名称生成的,使用DataBindingUtil.setContentView建立Activity和布局文件的关联关系。binding中持有了由控件id生成的对象,可以直接使用。这样在Activity中只需要持有binding就可以引用到所有的。精简了控件成员变量和findViewById。

private MainActivityViewModel viewModel;
private ActivityMainBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    viewModel = new ViewModelProvider(this).get(MainActivityViewModel.class);
    binding = DataBindingUtil.setContentView(this,R.layout.activity_main);
    binding.tvResult.setText(String.valueOf(viewModel.getResult()));
    final Observer<Integer> resultObserver = new Observer<Integer>() {
        @Override
        public void onChanged(Integer integer) {
            binding.tvResult.setText(String.valueOf(integer));
        }
    };
    viewModel.getLiveResult().observe(this, resultObserver);
    binding.button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            viewModel.getLiveResult().setValue(viewModel.getLiveResult().getValue() + 1);
        }
    });
}

(4)建立视图和ViewModel的双向绑定 上面实际上只是建立了Activity和View的绑定,还是需要在Activity中处理很多更新界面响应事件的代码。下面通过双向绑定来实现数据变动View自动刷新,View事件响应自动执行业务函数。 首先看我们定义的ViewModel

public class MainActivityViewModel extends ViewModel {
    private MutableLiveData<Integer> liveResult;

    public MutableLiveData<Integer> getLiveResult() {
        if (liveResult == null) {
            liveResult = new MutableLiveData<>();
            liveResult.setValue(0);// 设置默认值为0
        }
        return liveResult;
    }
    // 业务函数
    public void add() {
        liveResult.setValue(liveResult.getValue() + 1);
    }
}

注意:当成员变量私有这里的get方法之后连接的第一个字母必须是大写,不然xml文件中会提示找不到。 上面使用LiveData来存储业务数据,定义一个累加函数,通过setValue来更新数据。 在XML中data标签内引用刚定义的ViewModel,在XML中可以使用绑定表达式@{ 表达式 },其中可以调用绑定的数据对象的方法和成员变量,也可以使用算数、位运算、逻辑、赋值比较、三元运算符等常见的表达式形式。

// TextView中
android:text="@{String.valueOf(viewModel.liveResult)}"
// Buttion中
android:onClick="@{()->viewModel.add()}"

此时我们建立了ViewModel 在XML里面也调用了ViewModel里面的数据,但实际上并不能直接允许,这里只是声明,还需要再Activity/Fragment中建立ViewModel和View的绑定关系。

private MainActivityViewModel viewModel;
private ActivityMainBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // 创建ViewModel
    viewModel = new ViewModelProvider(this).get(MainActivityViewModel.class);
    // 创建ViewBinding
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    // 建立ViewModel实例和View的关联关系
    binding.setViewModel(viewModel);
    // 建立LiveData和Activity生命周期的同步
    binding.setLifecycleOwner(this);
}

注意: 跟数据直接相关的事件我们可以绑定在ViewModel中,但如果是和业务相关的逻辑,还是要在Act或Frag中来处理。

4.Navigation

Navigation是一个很有用的组件,这里可以很轻松的处理Fragment之间的跳转。 (1)第一步新建一个工程后首先引入依赖

    implementation 'androidx.navigation:navigation-fragment:2.3.0'
    implementation 'androidx.navigation:navigation-ui:2.3.0'

(2)创建多个Fragment,在业务上理清楚多个Fragment之间的关系,是如何跳转的。 (3)此时就正式开始使用Navigation了,在资源目录下创建navigation目录,类型选择navigation。然后创建一个Navigation Resources File ,这其实就是一张导航图,通过添加按钮New Destination 按照业务顺序将刚才创建的Fragment布局页面都添加进来。第一个添加的Fragment页面是起始页面,通过action连接线将多个页面的跳转关系连接起来。

(4)NavigationHost
使用Fragment总得有个容器,通常我们在主Activity的布局中。可以直接使用可视化布局工具,在Containers中有个NavHostFragment,拖动到布局中即可使用,实际生成的xml文件是 NavHost需要指定关联的NavGraph,所以默认显示的是图中的第一个Fragment

<fragment
    android:id="@+id/fragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:defaultNavHost="true"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:navGraph="@navigation/my_nav_graph" />

(5)NavController Fragment之间的切换一般是通过点击事件来切换的,现在可以通过NavController很方便的进行切换。

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        getView().findViewById(R.id.btn_home).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                NavController navController = Navigation.findNavController(v);// 找到这个btn 归属的NavController
                navController.navigate(R.id.action_homeFragment2_to_detailFragment2);
            }
        });
    }

NavController需要指定View来找到事件所归属的Controller,调用navigate方法找到图中的action,就可以按该路径进行跳转。

(6)传递数据 当然也可以添加Bundle来在页面之间传递数据。

// 起始Fragment
Bundle bundle = new Bundle();
bundle.putString("data","具体数据");
navController.navigate(R.id.action_homeFragment2_to_detailFragment2,bundle);
// 目标Fragment
String data =  getArguments().getString("data");

使用Bundle只能传递简单数据,这里还有一种更好的方式来实现数据传输,就是使用之前学到的ViewModel,依托于ViewModel的长生命周期,在不同Fragment中获取同一个ViewModel实例就可以共享数据了。 首先我们定义一个ViewModel

public class MyViewModel extends AndroidViewModel {
    public MyViewModel(@NonNull Application application) {
        super(application);
    }

    private MutableLiveData<Integer> number;

    public MutableLiveData<Integer> getNumber() {
        if (number == null) {
            number = new MutableLiveData<>();
            number.setValue(0);
        }
        return number;
    }

    public void add(int num) {
        getNumber().setValue(getNumber().getValue() + num);
        if (getNumber().getValue() == 0) {
            getNumber().setValue(0);
        }
    }
}

其次Fragment之间数据的展示我们也可以结合DataBinding来实现,在需要共享数据的Fragment布局中都添加ViewModel

<data>
    <variable
        name="data"
        type="com.example.navdemo3.MyViewModel" />
</data>

最后在Fragment中的onCreateView方法内部进行实例化,注意页面跳转是业务逻辑,这里的点击事件在Fragment中处理。数据变动的事件直接在xml中绑定就好。

MyViewModel viewModel = new ViewModelProvider(getActivity()).get(MyViewModel.class);
FragmentDetailBinding detailBinding = DataBindingUtil.inflate(inflater,R.layout.fragment_detail,container,false);
detailBinding.setData(viewModel);
detailBinding.setLifecycleOwner(getActivity());
detailBinding.button4.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        NavController controller= Navigation.findNavController(view);
        controller.navigate(R.id.action_detailFragment_to_homeFragment);
    }
});
return detailBinding.getRoot(); // 返回onCreateView方法所需的View

5.LifeCycle

先看一个案例,设计一个计时器,在界面暂停的时候暂停计时,界面恢复的时候继续计时。 界面部分很简单,使用Chronometer 这个控件即可。逻辑部分如下:

    private Chronometer mChronometer;
    private long elapsedTime;
    private SharedPreferences mShp;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mChronometer = findViewById(R.id.chronometer);
        //mChronometer.setBase(System.currentTimeMillis());// UNIX时间 1970-01-01
        mChronometer.setBase(SystemClock.elapsedRealtime());// 上一次启动运行的时间 毫秒 用作时间段统计
        mChronometer.start();

        mShp = getSharedPreferences("wux",MODE_PRIVATE);
    }

    @Override
    protected void onPause() {
        super.onPause();
        elapsedTime = SystemClock.elapsedRealtime() - mChronometer.getBase();
        mShp.edit().putLong("elapsedTime",elapsedTime).apply();
        mChronometer.stop();
    }

    @Override
    protected void onResume() {
        super.onResume();
        elapsedTime = mShp.getLong("elapsedTime",0l);
        mChronometer.setBase(SystemClock.elapsedRealtime()-elapsedTime);
        mChronometer.start();
    }

在Activity中使用一个变量来保存计时累计时间。一个很简单的功能还是要在Activity的生命周期方法中写几行代码,如果有多个控件都需要跟随生命周期进行状态变更,那么代码会全部揉在一起。

通常我们开发业务时会根据前台或后台组件特性(界面控件、缓存、工具类实例),在生命周期方法中调用组件的方法来实现数据刷新、状态暂存等功能。 这样做会导致生命周期中的代码越来越多,组件和调用方深度耦合,不利于功能组件复用。 生命周期感知型组件可以伴随宿主(比如Activity/Fragment)生命周期变化来调用组件内的生命周期。 通过将功能组件改造成生命周期感知型组件,可以让原本要在外部调用的方法放置在组件内部调用,通过观察者模式实现。 TODO 配图 这里主要涉及三个类: LifecycleObserver 接口:这个是作为观察者需要实现的接口。 LifecycleOwner 接口:这个是作为被观察者需要实现的接口。这个接口只有一个getLifecycle方法,返回一个Lifecycle 对象。

Lifecycle 抽象类:这里算是最终的被观察者,主要定义了addObserverremoveObservergetCurrentState 三个抽象方法以及Event和State枚举。 LifecycleRegistry 是LifeCycle的实现类,如果要自定义LifecycleOwner 通常会在宿主组件中创建一个LifecycleRegistry 实例,通过这个实例lifecycleRegistry.markState(Lifecycle.State.STARTED);来变更生命周期状态。

此处利用LifecycleObserver 来改造上面的例子。 继承原有组件,实现LifecycleObserver 接口。定义随生命周期调用的业务方法,添加注解标记要在什么生命周期方法中调用。

public class LifeChrometer extends Chronometer implements LifecycleObserver {
    private long elapsedTime;

    public LifeChrometer(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    private void pauseMeter() {
        elapsedTime = SystemClock.elapsedRealtime() - getBase();
        stop();
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    private void resumeMeter() {
        setBase(SystemClock.elapsedRealtime() - elapsedTime);
        start();
    }
}

由于AppCompatActivity已经实现了LifecycleOwner 接口,所以只需要调用 getLifecycle获取LifeCycle实例,再通过addObserver建立观察者和被观察的关联即可。

    private LifeChrometer mChronometer;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mChronometer = findViewById(R.id.chronometer);
        getLifecycle().addObserver(mChronometer);
    }

通过上面的改造就实现了让Chronometer 自动感知生命周期。

6.Room

是谷歌官方提供的数据库中间件,目的是为了更方便的使用数据。 要上手Room 基本搞懂三个概念就可以了 数据库对象 对应Database 表对象 对应XxxDao 表记录 对应Entry实体

先加一下依赖,这里参考官方文档

    def room_version = "2.2.5"
    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"
   
    // optional - RxJava support for Room
    implementation "androidx.room:room-rxjava2:$room_version"
    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation "androidx.room:room-guava:$room_version"
    // optional - Test helpers
    testImplementation "androidx.room:room-testing:$room_version"

(1)先创建实体类 @Entry 会标记这个类为一张表,可以再注解中设置表名 @PrimaryKey(autoGenerate = true) 标记为主键,设置自增 @ColumnInfo(name = "english_word") 标记这个字段在表里面的名称为english_word 实体类要用的注解主要就这几个

@Entity
public class Word {
    @PrimaryKey(autoGenerate = true)
    private int id;
    @ColumnInfo(name = "english_word")
    private String word;
    @ColumnInfo(name = "chinese_meaning")
    private String chineseMeaning;

    public Word(String word, String chineseMeaning) {
        this.word = word;
        this.chineseMeaning = chineseMeaning;
    }
}

(2)创建Dao Dao是Database access object 的意思,这个接口对象代表了一张表,里面定义的方法是针对这个表定义的操作。 针对一张表无非是增删改查,基本操作框架会通过注解自动生成对应SQL语句,如果是特殊操作则需要自己在@Query()注解中编写SQL语句。

@Dao // database access object
public interface WordDao {
    @Insert
    void insertWords(Word... words);

    @Update
    void updateWords(Word... words);

    @Delete
    void deleteWords(Word... words);

    @Query("DELETE FROM WORD")
    void deleteAllWords();

    @Query("SELECT * FROM WORD ORDER BY ID DESC")
    List<Word> getAllWords();
}

(3)创建Database Database是一个抽象类,需要继承RoomDatabase ,只需要定义提供Dao的抽象方法即可。

@Database(entities = {Word.class}, version = 1,exportSchema = false)
public abstract class WordDatabase extends RoomDatabase {
    public abstract WordDao getWordDao();
}

在Activity中使用Room,只需要使用Room.databaseBuilder就可以创建数据库对象,拿到Dao对象就可以使用我们刚才定义的方法。

private void initData() {
    mWordDatabase = Room.databaseBuilder(this, WordDatabase.class, "word_test_db_name")
            .allowMainThreadQueries()// 临时让存取操作可在主线程执行
            .build();
    mWordDao = mWordDatabase.getWordDao();
}

通过以上三步就可以使用数据库了,但还有需要优化的地方。 (1)WordDatabase的创建成本很高,最好使用单例模式创建。

@Database(entities = {Word.class}, version = 1,exportSchema = false)
public abstract class WordDatabase extends RoomDatabase {
    private static WordDatabase INSTANCE;
    public static final String DB_NAME="test_db";

    public synchronized static WordDatabase getWordDatabase(Context context) {
        if (INSTANCE==null){
            INSTANCE = Room.databaseBuilder(context.getApplicationContext(),WordDatabase.class,DB_NAME)
                    .allowMainThreadQueries()
                    .build();
        }
        return INSTANCE;
    }

    public abstract WordDao getWordDao();
}

(2)对于某些需要在前台界面即时改动的数据可以使用LiveData。 先修改Dao里面的方法,返回值设为LiveData类型。

    @Query("SELECT * FROM WORD ORDER BY ID DESC")
    LiveData<List<Word>> getAllWords();

在使用数据的地方绑定观察者,在onChanged内部处理因数据变动导致的UI变更。

allWordsLive = mWordDao.getAllWords();
allWordsLive.observe(this, new Observer<List<Word>>() {
    @Override
    public void onChanged(List<Word> words) {
        StringBuilder result = new StringBuilder();
        for (Word word : words) {
            result.append(word);
            result.append("\n");
        }
        mBinding.tvResult.setText(result.toString());
    }
});

(3)对数据库的操作不应该在主线程,之前是使用AsyncTask来实现短时间的异步任务的,从api31开始被废弃了。可以按如下方式进行使用。

static class ClearAsyncTask extends AsyncTask<Void, Void, Void> {
    private WordDao mWordDao;

    public ClearAsyncTask(WordDao wordDao) {
        mWordDao = wordDao;
    }

    @Override
    protected Void doInBackground(Void... voids) {
        mWordDao.deleteAllWords();
        return null;
    }
}

(4)将数据库相关逻辑都移动到ViewModel中去。

public class MyViewModel extends AndroidViewModel {
    WordDatabase mWordDatabase;
    WordDao mWordDao;
    LiveData<List<Word>> allWordsLive;

    public MyViewModel(@NonNull Application application) {
        super(application);
        mWordDatabase = WordDatabase.getWordDatabase(application);
        mWordDao = mWordDatabase.getWordDao();
        allWordsLive = mWordDao.getAllWords();
    }

    public LiveData<List<Word>> getAllWordsLive() {
        return allWordsLive;
    }

    public void insertWord(Word... words) {
        new InsertAsyncTask(mWordDao).execute(words);
    }
    。。。。。。。
}

(5)使用Repository模式来精简ViewModel 经过上面改造ViewModel中不仅包含很多数据库相关操作,这里官方推荐使用Repository来进一步简化ViewModel的职责。 这里其实也不复杂,就是将数据库相关打代码打包这里到这里,在ViewModel中进行调用。

public class WordRepository {
    private WordDatabase mWordDatabase;
    private WordDao mWordDao;
    private LiveData<List<Word>> allWordsLive;
    public WordRepository(Context context) {
        mWordDatabase = WordDatabase.getWordDatabase(context.getApplicationContext());
        mWordDao = mWordDatabase.getWordDao();
        allWordsLive = mWordDao.getAllWords();
    }
    public LiveData<List<Word>> getAllWordsLive() {
        return allWordsLive;
    }
    。。。。
}

(6)数据库升级 Migration 使用数据库就会遇到字段变更的问题。新旧版本更替时需要妥善处理好这些差异。 破坏性升级 只要数据库升级就清空当前数据重新创建表,

INSTANCE = Room.databaseBuilder(context.getApplicationContext(),WordDatabase.class,DB_NAME)
        .fallbackToDestructiveMigration()// 破坏性升级
        .build();

增量升级 如果对表添加了新的字段,需要使用sql语句修改表。 比如在Word实体表里面直接添加新字段

    @ColumnInfo(name = "new_row")
    private String newRow;

    public String getNewRow() {
        return newRow;
    }

    public void setNewRow(String newRow) {
        this.newRow = newRow;
    }

直接编译运行app会闪退,日志提示如下错误。

Caused by: 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)

提示我们要增加数据库版本号。那就把version+1

@Database(entities = {Word.class}, version = 2, exportSchema = false)
public abstract class WordDatabase extends RoomDatabase {...}

重新编译安装后还是会闪退,因为我们只是提升了数据库版本,但是没有配置升级策略。

Caused by: java.lang.IllegalStateException: A migration from 1 to 2 was required but not found.
Please provide the necessary Migration path via 
RoomDatabase.Builder.addMigration(Migration ...) or
 allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.

提示我们要么通过addMigration来添加Migration 类型的实例对象,要么配置fallbackToDestructiveMigration来进行破坏性升级。这里使用第一种方案

@Database(entities = {Word.class}, version = 2, exportSchema = false)
public abstract class WordDatabase extends RoomDatabase {
    private static WordDatabase INSTANCE;
    public static final String DB_NAME = "test_db";

    public synchronized static WordDatabase getWordDatabase(Context context) {
        if (INSTANCE == null) {
            INSTANCE = Room.databaseBuilder(context.getApplicationContext(), WordDatabase.class, DB_NAME)
                    .addMigrations(MIGRATION_1_2)
                    .build();
        }
        return INSTANCE;
    }

    public abstract WordDao getWordDao();

    static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE Word ADD COLUMN new_row TEXT");
        }
    };
}

这样就完成了版本迁移

减量升级 如果要删除某一个字段,由于sqlite不支持直接删除字段,这里的步骤就稍显麻烦,需要先创建一张删除了字段的临时表,再将旧表中的数据插入到新表中,删除旧表,重命名临时表为旧表。 (1)首先再实体类中将要删除的字段删除, (2)增加数据库版本 (3)创建升级策略并添加到addMigrations中去。

static final Migration MIGRATION_3_4 = new Migration(3,4) {
    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) {
        // 1.创建一张临时表
        database.execSQL("CREATE TABLE word_temp (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL , " +
                "english_word TEXT ," +
                "chinese_meaning TEXT," +
                "new_row TEXT)");
        // 2.将旧表中的数据复制到新表中
        database.execSQL("INSERT INTO word_temp (id,english_word,chinese_meaning,new_row) " +
                "SELECT id,english_word,chinese_meaning,new_row FROM word");
        // 3.删除旧表
        database.execSQL("DROP TABLE word");
        // 4.将临时表名改为旧表名
        database.execSQL("ALTER TABLE word_temp RENAME TO word");
    }
};

7.BottomNavitation

用于搭建底部导航栏以及页面切换的框架。 (1)开发底部导航栏 底部导航栏实际上是用menu实现的,主要配置显示的文字和图片。 这里需要注意的是配置的id需要和nav_graph中对应fragment的id一致。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/first_fragment"
        android:icon="@drawable/ic_baseline_4k_24"
        android:title="旋转" />
    <item
        android:id="@+id/second_fragment"
        android:icon="@drawable/ic_baseline_ac_unit_24"
        android:title="缩放" />
    <item
        android:id="@+id/third_fragment"
        android:icon="@drawable/ic_baseline_access_alarm_24"
        android:title="移动" />
</menu>

(2)创建三个fragment,这里可以使用模板顺带创建好fragment关联的viewmodel。

(3)创建nav_graph,这里只需要将三个fragment布局移入即可,无需配置连接关系,因为这三个页面实际上是平级的。这里每个页面的id需要和底部menuitem的id一一对应。

(4)再布局层面将底部栏和fragment容器配置好。 底部栏使用的BottomNavigationView控件,需要配置menu为前面创建的menu文件。 fragment的宿主使用NavHostFragment ,注意配置对应的nav_graph文件。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".MainActivity2">

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottomNavigationView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="#E8F5E9"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:menu="@menu/bottom_menu" />

    <fragment
        android:id="@+id/fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="1dp"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/bottom_nav" />
</androidx.constraintlayout.widget.ConstraintLayout>

完成上的步骤就完成了布局层面的搭建,这里还需要在代码层面做关联。

(5)在activity中将底部导航和fragment容器装配在一起。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main2);
    // 拿到底部栏
    BottomNavigationView bottomNavigationView = findViewById(R.id.bottomNavigationView);
    // 生成fragment容器关联的NavController
    NavController navController = Navigation.findNavController(this, R.id.fragment);
    // 拿到底部栏配置
    AppBarConfiguration configuration = new AppBarConfiguration.Builder(bottomNavigationView.getMenu()).build();
    // 将controller和configuration装配到activity中
    NavigationUI.setupActionBarWithNavController(this, navController, configuration);
    // 将底部栏和controller装配到一起
    NavigationUI.setupWithNavController(bottomNavigationView, navController);
}

完成上述步骤就实现了底部切换页面的框架。

8.Paging

paging是分页的意思,一次加载一部分数据来减少网络资源和系统资源的使用。

(1)先引入依赖

    def paging_version = "2.1.2"
    implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx
    // alternatively - without Android dependencies for testing
    testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx
    // optional - RxJava support
    implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx

(2)使用分页加载数据库中的数据 针对需要分页的查询,在Dao接口中返回DataSource.Factory类型,需指定具体元素类型。

    // 使用分页加载数据库中数据
    @Query("SELECT * FROM student_table ORDER BY id")
    DataSource.Factory<Integer,Student> getAllStudents();

(3)使用支持分页加载的Adapter 这里我们使用Recycle View展示需要分页加载的内容,这里的Adapter需要继承PagedListAdapter。 这里和ListAdapter类似,需要定义数据差异。

public class PageAdapter extends PagedListAdapter<Student, PageAdapter.MyViewHolder> {

    public PageAdapter() {
        super(new DiffUtil.ItemCallback<Student>() {
            // 定义什么样的数据是完全一致的
            @Override
            public boolean areItemsTheSame(@NonNull Student oldItem, @NonNull Student newItem) {
                return oldItem.getId() == newItem.getId();
            }
            // 定义什么样的数据是内容相同的
            @Override
            public boolean areContentsTheSame(@NonNull Student oldItem, @NonNull Student newItem) {
                return oldItem.getStudentNumber() == newItem.getStudentNumber();
            }
        });
    }

    protected PageAdapter(@NonNull AsyncDifferConfig<Student> config) {
        super(config);
    }

    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_data, parent, false);
        return new MyViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
        Student student = getItem(position);
        if (student == null) {
            holder.textView.setText("loading");
        } else {
            holder.textView.setText(String.valueOf(student.getStudentNumber()));
        }
    }

    static class MyViewHolder extends RecyclerView.ViewHolder {
        TextView textView;
        public MyViewHolder(@NonNull View itemView) {
            super(itemView);
            textView = itemView.findViewById(R.id.tv_item);
        }
    }
}

(4)绑定数据源 这里以用LiveData的方式来更新数据,

LiveData<PagedList<Student>> allStudentLivePaged;

mDatabase = StudentDatabase.getInstance(this);
mStudentDao = mDatabase.getStudentDao();
allStudentLivePaged = new LivePagedListBuilder<>(mStudentDao.getAllStudents(), 3).build();
allStudentLivePaged.observe(this, new Observer<PagedList<Student>>() {
    @Override
    public void onChanged(final PagedList<Student> students) {
        mPageAdapter.submitList(students);
    }
});

9. Volley

Vollery 适合数据量不大,请求频繁的场景。有缓存机制、可以自定义请求、可取消请求、回调会自动切换回主线程。 (0)添加权限

<uses-permission android:name="android.permission.INTERNET"/>

(1)添加依赖

implementation 'com.android.volley:volley:1.1.1'

(2)创建RequestQueue RequestQueue管理着一个线程池,为添加进队列的请求分配线程,在收到响应后将回调切回主线程。 最简单的创建方式

RequestQueue queue = Volley.newRequestQueue(context);

RequestQueue是比较耗资源的,所以一般封装一下创建为单例。 也可以自定义缓存、网络、线程池等。

// 创建缓存
File cacheDir = new File(this.getCacheDir(), "volleyCacheDir");
DiskBasedCache diskBasedCache = new DiskBasedCache(cacheDir);

// 创建网络,HurlStack内部发请求还是依赖HttpURLConnection
BasicNetwork network = new BasicNetwork(new HurlStack());

// 创建线程池数量,系统默认就是4
int threadPoolSize = 4;

// 创建ResponseDelivery来处理响应和错误,并切换线程
ResponseDelivery responseDelivery = new ExecutorDelivery(new Handler(Looper.getMainLooper()));

// 装配上述功能并创建RequestQueue
RequestQueue queue1 = new RequestQueue(diskBasedCache,network,threadPoolSize,responseDelivery);
// 启动该queue
queue1.start();

(3)创建请求 Volley官方提供的三种基本的请求 StringRequest 、JsonObjectRequest、JsonArrayRequest 来满足不同场景。 如果是普通的get请求,不需要带参数可以直接使用StringRequest。

String url = "https://www.baidu.com";
StringRequest stringRequest = new StringRequest(
        Request.Method.GET,
        url,
        new Response.Listener<String>() {
            @Override
            public void onResponse(String response) {
                mActivityMainBinding.tvResult.setText(response);
            }
        },
        new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.d(TAG, "onErrorResponse: "+error);
            }
        });

如果是请求体带参数可以使用JsonObjectRequest来请求。

JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(
        Request.Method.POST,
        url,
        requestArgs,
        new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {

            }
        },
        new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {

            }
        });

(4)发起请求

queue.add(jsonObjectRequest);

(5)取消请求 如果要对请求做撤回处理,可以使用cancel

// 在请求发送前打tag
jsonObjectRequest.setTag(TAG);

// 在想取消请求的地方调用cancel,比如手动点击、页面退出回调
if (queue!=null){
    queue.cancelAll(TAG);
}

// 也可以设置过滤条件来取消符合条件的请求
RequestQueue.RequestFilter requestFilter = new RequestQueue.RequestFilter() {
    @Override
    public boolean apply(Request<?> request) {
        // 根据请求字段来进行取消条件的筛选
        return false;
    }
};
        
if (queue!=null){
    queue.cancelAll(requestFilter);
}

(6)加载图片 volley有一个可以ImageLoader工具类,可以用于请求图片地址并加载图片

RequestQueue queue = Volley.newRequestQueue(this);
// 图片地址
String url = "http://s1.dgtle.com/dgtle_img/article/2020/02/23/6c8c120200223130659151_1800_500.jpeg";
// 内存缓存
LruCache<String,Bitmap> lruCache = new LruCache<>(50);
// 创建Image Loader并添加缓存逻辑
ImageLoader imageLoader=new ImageLoader(queue, new ImageLoader.ImageCache() {
    @Override
    public Bitmap getBitmap(String url) {
        return lruCache.get(url);
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        lruCache.put(url,bitmap);
    }
});
// 使用imageLoader请求图片地址并监听响应
imageLoader.get(url, new ImageLoader.ImageListener() {

    @Override
    public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) {
        mActivityMainBinding.imageView.setImageBitmap(response.getBitmap());
    }

    @Override
    public void onErrorResponse(VolleyError error) {
        mActivityMainBinding.imageView.setImageResource(R.drawable.ic_baseline_error_24);
    }
});

10.Glide

Glide是

(1)引入依赖

implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'

(2)加载图片

Glide.with(MainActivity.this)
        .load(url)
        .placeholder(R.drawable.ic_baseline_error_24)
        .listener(new RequestListener<Drawable>() {
            @Override
            public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
                return false;
            }

            @Override
            public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
                return false;
            }
        }).into(mActivityMainBinding.imageView);

11.Swiperefreshlayout

SwipeRefreshLayout是官方提供的下拉刷新布局,可以很轻松的实现下拉刷新! (1)引入依赖

implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"

(2)在布局中引入控件

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:id="@+id/srl_test"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_marginTop="8dp"
    android:layout_marginBottom="32dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.0"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintVertical_bias="1.0" >

    <androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

(3)为其设置刷新监听 使用setRefreshing可以开启或关闭刷新弹窗。

mMain2Binding.srlTest.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
    @Override
    public void onRefresh() {
        Toast.makeText(MainActivity2.this, "正在刷新", Toast.LENGTH_LONG).show();
        new Handler(getMainLooper()).postDelayed(new Runnable() {
            @Override
            public void run() {
                mMain2Binding.srlTest.setRefreshing(false);
            }
        }, 3000);
    }
});

todo

ListAdapter