Jetpack组件系列文章
Android架构之LifeCycle组件
Android架构之Navigation组件(一)
Android架构之Navigation组件(二)
Android架构之Navigation组件(三)
Android架构之Navigation组件(四)
Android架构之ViewModel组件
Android架构之LiveData组件
Android架构之Room组件(一)
Android架构之Room组件(二)
Android架构之WorkManager组件
Android架构之DataBinding(一)
Android架构之DataBinding(二)
Android架构之Paging组件(一)
Android架构之Paging组件(二)
Jetpack与MVVM架构
MVVM即Model-View-ViewModel的缩写。它的出现是为了将图形界面与业务逻辑、数据模型进行解耦。MVVM也是Google推崇的一种Android项目架构模型。我们前面所学习的Jetpack组件,大部分都是为了能够更好地架构MVVM应用程序而设计的。
MVVM架构的应用程序采用了数据模型驱动界面更新的设计方案。我们希望数据在发生变化时,界面能够自动得到通知并进行更新,这就是数据模型驱动界面更新。对于普通应用程序,数据的来源无非就两种,一种来源于本地,通常是本地数据库;另外一种来源于远程服务端,即网路数据。
从上图可以看成,ViewModel的数据既可以来源于本地数据库,也可以来源于远程服务器。但在实际开发过程中,我们会将本地数据库和远程服务器两种方式进行结合。采用单一原则,只从数据库中获取数据。因此,可以在ViewModel层和Model层之间引入Repository层。在Repository层处理本地数据和网络数据之间的业务逻辑,让Repository层对ViewModel层负责,使ViewModel只需要关心自己的业务逻辑,而不同关心数据的具体来源。
有了LifeCycle组件,当系统组件Activity、Fragment、Service和Application的生命周期发生变化时,我们的自定义组件能够及时得到通知。LifeCycle使我们的自定义组件与系统组件进一步解耦。
处理导航图所需的一切,包括页面的跳转、参数的传递、动画效果的设置,以及App bar的设置等。导航图让我们可以站在"上帝的视角",俯瞰应用程序所有界面之间的关系。
ViewModel负责处理和存放View与Model之间的业务逻辑。它之直接对UI界面所需的数据负责,让视图和数据进行分离。并且,ViewModel与生命周期相关,它能自动处理由于屏幕旋转导致界面重新创建所带来的数据重新获取问题。
LiveData在MVVM架构的层与层之间扮演者桥梁的作用。当数据发送变化时,通过LiveData让数据的订阅者得到通知。
Google官方的ORM数据库,原生支持LiveData.在搭配LiveData使用时,当Room数据库中的数据发送变化,当LiveData是数据的订阅者能够及时得到通知,而无须从数据库重新获取数据。
为应用程序中那些不需要及时完成的任务提供统一的解决方案。
进一步解耦UI界面。DataBinding的出现让findViewById不复存在,使布局文件能够承担更多的工作,甚至能承担一些简单的业务逻辑,这减轻了Activity/Fragment的工作量。
为常见的3种分页机制提供了统一的解决方法,使我们能够将更多的精力专注在业务代码上。
下面我们将通过实际案例,使用Jetpack组件搭建符合MVVM架构的应用程序。
我们将用到的Jetpack组件有LiveData、Room、ViewModel和DataBinding.对于搭建一个简单的MVVM应用程序,这已经足够了。
通过GitHub API获取某个特定的用户的个人信息并进行展示。
https://api.github.com/users/michaelye
对项目层级进行说明:
在app的build.gradle文件中添加相关依赖。
//启用DataBinding
android {
buildFeatures{
dataBinding = true
// for view binding :
// viewBinding = true
}
}
//导入Room
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
//导入viewModel
implementation "androidx.lifecycle:lifecycle-viewmodel:2.2.0"
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
//导入retrofit
implementation "com.squareup.retrofit2:retrofit:2.6.2"
implementation "com.squareup.retrofit2:converter-gson:2.6.2"
//导入glide
implementation 'com.github.bumptech.glide:glide:4.9.0'
//下拉刷新
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
//导入圆形头像库
implementation 'de.hdodenhof:circleimageview:3.0.1'
首先需要定义User模型,该模型在整个应用程序中都需要被用到。模型中的字段来源于GitHub API 接口所返回的数据,其中只保留我们所需要的字段。
定义User类
@Entity(tableName = "user")
public class User {
@PrimaryKey
@ColumnInfo(name="id",typeAffinity = ColumnInfo.INTEGER)
public int id;
@ColumnInfo(name="name",typeAffinity =ColumnInfo.TEXT)
public String name;
@ColumnInfo(name = "avator",typeAffinity = ColumnInfo.TEXT)
@SerializedName("avatar_url")
public String avatar;
@ColumnInfo(name = "followers",typeAffinity = ColumnInfo.INTEGER)
public int followers;
@ColumnInfo(name = "following",typeAffinity = ColumnInfo.INTEGER)
public int following;
@ColumnInfo(name = "blog",typeAffinity = ColumnInfo.TEXT)
public String blog;
@ColumnInfo(name = "company",typeAffinity = ColumnInfo.TEXT)
public String company;
@ColumnInfo(name = "bio",typeAffinity = ColumnInfo.TEXT)
public String bio;
@ColumnInfo(name = "location",typeAffinity = ColumnInfo.TEXT)
public String location;
@ColumnInfo(name = "htmlUrl",typeAffinity = ColumnInfo.TEXT)
@SerializedName("html_url")
public String htmlUrl;
public User(int id, String name, String avatar, int followers, int following, String blog, String company, String bio, String location, String htmlUrl) {
this.id = id;
this.name = name;
this.avatar = avatar;
this.followers = followers;
this.following = following;
this.blog = blog;
this.company = company;
this.bio = bio;
this.location = location;
this.htmlUrl = htmlUrl;
}
}
@Database(entities = {User.class},version = 1 )
public abstract class UserDatabase extends RoomDatabase {
private static final String DATABASE_NAME = "user_db";
private static UserDatabase userDatabase;
public static synchronized UserDatabase getInstance(Context context){
if(userDatabase==null){
userDatabase = Room.databaseBuilder(
context.getApplicationContext(),
UserDatabase.class,
DATABASE_NAME)
.build();
}
return userDatabase;
}
public abstract UserDao userDao();
}
创建用于访问User表的Dao文件。注意,在查询方法中返回的是LiveData包装过的User,这样,当Room中的数据发生变化时,能够自动通知数据观察者。
@Dao
public interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertUser(User user);
@Delete
void deleteStudent(User user);
@Query("SELECT * FROM user WHERE name=:name")
LiveData<User> getUserByName(String name);
}
public interface Api {
@GET("users/{userId}")
Call<User> getUser(
@Path("userId") String userId
);
}
public class RetrofitClient {
private static final String BASE_URL = "https://api.github.com/";
private static RetrofitClient retrofitClient;
private Retrofit retrofit;
public RetrofitClient() {
retrofit = new Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
public static synchronized RetrofitClient getInstance(){
if(retrofitClient == null){
retrofitClient = new RetrofitClient();
}
return retrofitClient;
}
public Api getApi(){
return retrofit.create(Api.class);
}
}
我们已经定义好了Room和Retrofit,便可以在Application中对其进行实例化了。这样做的好处在于,方便我们统一管理和全局调用这两个对象,并且不同担心这两个对象的生命周期问题。
public class MyApplication extends Application {
private static UserDatabase userDatabase;
private static Api api;
@Override
public void onCreate() {
super.onCreate();
userDatabase = UserDatabase.getInstance(this);
api = RetrofitClient.getInstance().getApi();
}
public static UserDatabase getUserDatabase() {
return userDatabase;
}
public static Api getApi() {
return api;
}
}
注意:创建好Application之后,需求去Manifest文件中注册一下。
在该层请求网络数据,并将得到的数据写入Room数据库。需要注意的是,该类只对ViewModel负责。它提供了两个方法getUser()和refresh().当ViewModel需要数据时,不用关心数据的来源。由于使用了LiveData,当数据发生变化时,ViewModel会自动得到通知,因此 ,不需担心数据更新的问题
public class UserRepository {
private String TAG = this.getClass().getName();
private UserDao userDao;
private Api api;
public UserRepository(UserDao userDao, Api api) {
this.userDao = userDao;
this.api = api;
}
public LiveData<User> getUser(final String name){
refresh(name);
return userDao.getUserByName(name);
}
public void refresh(String name) {
api.getUser(name).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
if(response.body()!=null){
//存储到数据库中
insertUser(response.body());
}
}
@Override
public void onFailure(Call<User> call, Throwable t) {
}
});
}
public void insertUser(User body) {
//开启工作线程,插入数据到数据库
AsyncTask.execute(new Runnable() {
@Override
public void run() {
userDao.insertUser(body);
}
});
}
}
在ViewModel的构造器中,实例化Repository对象,并将数据库对象和Retrofit对象以构造器参数的形式传入Repository中。最后,同样还是利用LiveData,将User数据传递到上一层,即View层。
public class UserViewModel extends AndroidViewModel {
private LiveData<User> user;
private UserRepository userRepository;
private String userName = "MichaelYe";
public UserViewModel(@NonNull Application application) {
super(application);
UserDatabase database = MyApplication.getUserDatabase();
UserDao userDao = database.userDao();
userRepository = new UserRepository(userDao,MyApplication.getApi());
user = userRepository.getUser(userName);
}
public LiveData<User> getUser() {
return user;
}
public void refresh(){
userRepository.refresh(userName);
}
}
在Activity中,我们使用了DataBinding组件和下拉刷新组件,当User数据发送变化时,自动通过回调方法得到数据,在接收到通知后,将数据交给布局文件进行处理。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(
this,R.layout.activity_main);
final UserViewModel userViewModel = new ViewModelProvider(this,ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication())).get(UserViewModel.class);
userViewModel.getUser().observe(this, new Observer<User>() {
@Override
public void onChanged(User user) {
if(user!=null){
activityMainBinding.setUser(user);
}
}
});
finish();
SwipeRefreshLayout swipeRefresh = activityMainBinding.swipeRefresh;
swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
userViewModel.refresh();
swipeRefresh.setRefreshing(false);
}
});
}
}
在布局文件在处理Activity传递来的数据对象。其中用到了CircleImageView组件,用于生成圆形头像。
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.example.mvvc.model.User" />
</data>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_height="match_parent"
android:layout_width="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#dfdfdf"
>
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="128dp"
android:layout_marginLeft="28dp"
android:layout_marginRight="28dp"
android:background="@android:color/white"/>
<LinearLayout
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"
android:gravity="center_horizontal"
android:layout_marginTop="80dp"
android:orientation="vertical"
tools:context=".MainActivity">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/profile_image"
android:layout_width="96dp"
android:layout_height="96dp"
app:image="@{user.avatar}"
app:civ_border_width="2dp"
app:civ_border_color="#cccccc"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}"
android:textSize="22sp"
android:textStyle="bold"
android:layout_marginTop="8dp"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="16sp"
android:text="@{user.bio}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="16sp"
android:text="@{user.company}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="16sp"
android:text="@{user.location}"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="16sp"
android:text="@{user.htmlUrl}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginLeft="12dp"
android:textSize="16sp"
android:text="@{user.htmlUrl}"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="16sp"
android:text="@{user.htmlUrl}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="16sp"
android:text="@{user.blog}"/>
</LinearLayout>
</RelativeLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
</layout>
需要注意的是,在使用Activity传递过来的数据之前,先在布局最外层添加
build成功后,就可以开始数据填充了。在布局文件中,我们还通过自定义BindingAdapter,实现图片的加载,具体的加载工作,交给Glide完成。
public class ImageViewBindingAdapter {
@BindingAdapter(value = {"image","defaultImageResource"},requireAll = false)
public static void setImage(ImageView image,String imageUrl,int imageResource){
if(!TextUtils.isEmpty(imageUrl)){
Glide.with(image.getContext())
.load(imageUrl)
.placeholder(R.drawable.ic_launcher_background)
.into(image);
}else{
image.setImageResource(imageResource);
}
}
}
好了,Jetpack组件实现MVVM架构就到这里了,不足之处,欢迎大家留言,谢谢!