以下是关于JetPack学习的一些笔记
目录:
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 抽象类:这里算是最终的被观察者,主要定义了addObserver
、removeObserver
、getCurrentState
三个抽象方法以及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);
}
});
12.Hilt
首先在项目中引入依赖
总工程中
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.36'
在APP中
implementation 'com.google.dagger:hilt-android:2.36'
annotationProcessor 'com.google.dagger:hilt-compiler:2.36'
todo
ListAdapter