Jetpack与MVVM架构

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架构与Jetpack

MVVM即Model-View-ViewModel的缩写。它的出现是为了将图形界面与业务逻辑、数据模型进行解耦。MVVM也是Google推崇的一种Android项目架构模型。我们前面所学习的Jetpack组件,大部分都是为了能够更好地架构MVVM应用程序而设计的。

数据模型驱动页面更新

MVVM架构的应用程序采用了数据模型驱动界面更新的设计方案。我们希望数据在发生变化时,界面能够自动得到通知并进行更新,这就是数据模型驱动界面更新。对于普通应用程序,数据的来源无非就两种,一种来源于本地,通常是本地数据库;另外一种来源于远程服务端,即网路数据。
Jetpack与MVVM架构_第1张图片
从上图可以看成,ViewModel的数据既可以来源于本地数据库,也可以来源于远程服务器。但在实际开发过程中,我们会将本地数据库和远程服务器两种方式进行结合。采用单一原则,只从数据库中获取数据。因此,可以在ViewModel层和Model层之间引入Repository层。在Repository层处理本地数据和网络数据之间的业务逻辑,让Repository层对ViewModel层负责,使ViewModel只需要关心自己的业务逻辑,而不同关心数据的具体来源。
Jetpack与MVVM架构_第2张图片

回顾Jetpack组件

LifeCycle

有了LifeCycle组件,当系统组件Activity、Fragment、Service和Application的生命周期发生变化时,我们的自定义组件能够及时得到通知。LifeCycle使我们的自定义组件与系统组件进一步解耦。

Navigation

处理导航图所需的一切,包括页面的跳转、参数的传递、动画效果的设置,以及App bar的设置等。导航图让我们可以站在"上帝的视角",俯瞰应用程序所有界面之间的关系。

ViewModel

ViewModel负责处理和存放View与Model之间的业务逻辑。它之直接对UI界面所需的数据负责,让视图和数据进行分离。并且,ViewModel与生命周期相关,它能自动处理由于屏幕旋转导致界面重新创建所带来的数据重新获取问题。

LiveData

LiveData在MVVM架构的层与层之间扮演者桥梁的作用。当数据发送变化时,通过LiveData让数据的订阅者得到通知。

Room

Google官方的ORM数据库,原生支持LiveData.在搭配LiveData使用时,当Room数据库中的数据发送变化,当LiveData是数据的订阅者能够及时得到通知,而无须从数据库重新获取数据。

WorkManager

为应用程序中那些不需要及时完成的任务提供统一的解决方案。

DataBinding

进一步解耦UI界面。DataBinding的出现让findViewById不复存在,使布局文件能够承担更多的工作,甚至能承担一些简单的业务逻辑,这减轻了Activity/Fragment的工作量。

Paging

为常见的3种分页机制提供了统一的解决方法,使我们能够将更多的精力专注在业务代码上。

下面我们将通过实际案例,使用Jetpack组件搭建符合MVVM架构的应用程序。

使用Jetpack组件构建MVVM应用程序。

我们将用到的Jetpack组件有LiveData、Room、ViewModel和DataBinding.对于搭建一个简单的MVVM应用程序,这已经足够了。

案例分析

通过GitHub API获取某个特定的用户的个人信息并进行展示。

API接口

https://api.github.com/users/michaelye

项目工程结构

Jetpack与MVVM架构_第3张图片

对项目层级进行说明:

  1. Model: 使用Room数据库存储用户个人信息
  2. API:通过Retrofit库请求GitHub API
  3. Application: 在其中实例化Room和Retrofit对象,以便统一管理和全局调用
  4. Repository: 构建Repository层,用于处理Room中的数据与API接口得来的网络数据之间的业务关系。
  5. ViewModel: 从Repository层中获取数据,ViewModel不需要关心数据的来源是Room还是API接口
  6. View:我们的Activity和布局文件,将会用到DataBinding组件

准备工作

1.导入依赖

在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'

Model层

首先需要定义User模型,该模型在整个应用程序中都需要被用到。模型中的字段来源于GitHub API 接口所返回的数据,其中只保留我们所需要的字段。

GitHub API所返回的数据如下:
Jetpack与MVVM架构_第4张图片

定义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;
    }
}

创建Room数据库和Dao文件

 @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);
}

记得build一下,生成下图两个类。在这里插入图片描述

Api层

定义Api接口

public interface Api {
    @GET("users/{userId}")
    Call<User> getUser(
      @Path("userId") String userId
    );
}

实例化Retrofit对象

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);
    }
}

Application

我们已经定义好了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文件中注册一下。
在这里插入图片描述

Repository层

在该层请求网络数据,并将得到的数据写入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层

在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);
    }
}

View层

在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一下,会生成如下文件:
Jetpack与MVVM架构_第5张图片
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架构_第6张图片

好了,Jetpack组件实现MVVM架构就到这里了,不足之处,欢迎大家留言,谢谢!

你可能感兴趣的:(Android,Jetpack)