十四、架构模式

概观

本章将向您介绍可用于您的安卓项目的架构模式。它涵盖了使用 MVVM ( 模型-视图-视图模型)模式、添加视图模型以及使用数据绑定。您还将了解如何使用存储库模式来缓存数据,以及如何使用工作管理器来安排数据检索和存储。

到本章结束时,您将能够使用 MVVM 和数据绑定来构建您的安卓项目。您还可以使用带有房间库的存储库模式来缓存数据,并使用工作管理器按计划的时间间隔获取和保存数据。

简介

在前一章中,您学习了如何使用 RxJava 和 coroutines 进行后台操作和数据操作。现在,您将学习架构模式,以便改进应用。

开发安卓应用时,您可能倾向于将大部分代码(包括业务逻辑)编写在活动或片段中。这将使您的项目很难在以后进行测试和维护。随着项目的增长和变得更加复杂,难度也会增加。您可以用架构模式来改进您的项目。

架构模式是设计和开发部分应用的通用解决方案,尤其是大型应用。您可以使用体系结构模式将项目组织成不同的层(表示层、用户界面 ( 用户界面)层和数据层)或功能(观察者/可观察的)。有了架构模式,您可以用一种更容易开发、测试和维护的方式来组织代码。

对于安卓开发,常用的模式有 MVC ( 模型-视图-控制器)、 MVP ( 模型-视图-演示者)和 MVVM。谷歌推荐的架构模式是 MVVM,这将在本章中讨论。您还将了解数据绑定、使用房间库的存储库模式以及工作管理器。

让我们从 MVVM 的建筑模式开始。

MVVM

MVVM 允许你分离用户界面和业务逻辑。当你需要重新设计 UI 或者更新 Model/business logic 的时候,你只需要触碰相关的组件,而不会影响到你应用的其他组件。这将使您更容易添加新功能和测试现有代码。MVVM 在创建使用大量数据和视图的大型应用方面也很有用。

使用 MVVM 架构模式,您的应用将被分成三个组件:

  • 模型:代表数据层
  • 查看:显示数据的界面
  • 视图模型:从Model获取数据并提供给View

通过下图可以更好地理解 MVVM 的建筑模式:

Figure 14.1: The MVVM architectural pattern

图 14.1:MVVM 的建筑模式

该模型包含应用的数据和业务逻辑。用户看到和交互的活动、片段和布局就是 MVVM 的视图。视图只处理应用的外观。它们让视图模型知道用户的动作(比如打开一个活动或者点击一个按钮)。

视图模型链接视图和模型。视图模型从模型中获取数据,并将其转换以显示在视图中。视图订阅视图模型,并在值改变时更新用户界面。

您可以使用 Jetpack 的视图模型为您的应用创建视图模型类。Jetpack 的 ViewModel 管理自己的生命周期,所以你不需要自己处理。

通过在app/build.gradle文件依赖项中添加以下代码,可以将视图模型添加到项目中:

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

例如,如果你正在开发一个显示电影的应用,你可能会有一个MovieViewModel。该视图模型将具有获取电影列表的功能:

class MovieViewModel : ViewModel() {
    private val movies: MutableLiveData<List<Movie>>
    fun getMovies(): LiveData<List<Movie>> { ... }
    ...
}

在您的活动中,您可以使用ViewModelProvider创建视图模型:

class MainActivity : AppCompatActivity() {
    private val movieViewModel by lazy {
        ViewModelProvider(this).get(MovieViewModel::class.java)
    }
    ...
}

然后可以从 ViewModel 订阅getMovies功能,当电影列表发生变化时,在 UI 中自动更新列表:

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    movieViewModel.getMovies().observe(this, Observer { popularMovies ->
        movieAdapter.addMovies(popularMovies)
    })
    ...
}

当视图模型中的值发生变化时,视图会得到通知。您还可以使用数据绑定将视图与视图模型中的数据连接起来。在下一节中,您将了解更多关于数据绑定的信息。

数据绑定

数据绑定将布局中的视图链接到来自源(如视图模型)的数据。数据绑定可以自动为您处理视图,而不是添加代码来查找布局文件中的视图,并在视图模型的值发生变化时更新它们。

要在您的安卓项目中使用数据绑定,您应该在app/build.gradle文件的android块中添加以下内容:

buildFeatures {
    dataBinding true
}

在布局文件中,必须用布局标记包装根元素。在布局标签中,您需要为要绑定到该布局文件的数据定义data元素:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="movie" type="com.example.model.Movie"/>
    </data>
    <ConstraintLayout ... />
</layout>

电影布局变量表示将在布局中显示的com.example.model.Movie类。要设置数据模型中字段的属性,需要使用@{}语法。例如,要使用电影的标题作为TextView的文本值,可以使用以下内容:

<TextView
    ...
    android:text="@{movie.title}"/>

您还需要更改您的活动文件。如果您的布局文件名为activity_movies.xml,数据绑定库将在项目的构建文件中生成名为ActivityMainBinding的绑定类。在活动中,您可以用以下内容替换行setContentView(R.layout.activity_movies):

val binding: ActivityMoviesBinding = DataBindingUtil.setContentView(this,   R.layout.activity_movies)

也可以使用绑定类或DataBindingUtil类的inflate方法:

val binding: ActivityMoviesBinding =   ActivityMoviesBinding.inflate(getLayoutInflater())

然后,您可以使用名为movie的布局变量在布局中设置要绑定的movie实例:

val movieToDisplay = ...
binding.movie = movieToDisplay

如果使用LiveData作为绑定到布局的项,需要设置绑定变量的lifeCycleOwnerlifeCycleOwner规定了LiveData对象的范围。您可以将该活动用作绑定类的lifeCycleOwner:

binding.lifeCycleOwner = this

这样,当视图模型中LiveData的值改变时,视图将自动更新为新值。

您可以在文本视图中用android:text="@{movie.title}"设置电影标题。数据绑定库有默认的绑定适配器来处理与android:text属性的绑定。有时,没有可以使用的默认属性。您可以创建自己的绑定适配器。例如,如果您想为RecyclerView绑定电影列表,您可以创建一个自定义的BindingAdapter调用:

@BindingAdapter("list")
fun bindMovies(view: RecyclerView, movies: List<Movie>?) {
    val adapter = view.adapter as MovieAdapter
    adapter.addMovies(movies ?: emptyList())
}

这将允许您向接受电影列表的RecyclerView添加一个app:list属性:

app:list="@{movies}"

让我们尝试在安卓项目上实现数据绑定。

练习 14.01:在安卓项目中使用数据绑定

在前一章中,您使用电影数据库应用编程接口开发了一个显示热门电影的应用。在这一章中,您将使用 MVVM 改进应用。您可以使用上一章中的“热门电影”项目或制作它的副本。在本练习中,您将添加数据绑定,将视图模型中的电影列表绑定到用户界面:

  1. 在 AndroidStudio 打开Popular Movies项目。
  2. Open the app/build.gradle file and add the following in the android block:

    kt buildFeatures { dataBinding true }

    这将为您的应用启用数据绑定。

  3. Add the kotlin-kapt plugin at the end of the plugins block in your app/build.gradle file:

    kt plugins { ... id 'kotlin-kapt' }

    kotlin-kapt 插件是使用数据绑定所需的 kotlin 注释处理工具。

  4. Create a new file called RecyclerViewBinding that contains the binding adapter for the RecyclerView list:

    kt @BindingAdapter("list") fun bindMovies(view: RecyclerView, movies: List<Movie>?) { val adapter = view.adapter as MovieAdapter adapter.addMovies(movies ?: emptyList()) }

    这将允许您为RecyclerView添加一个app:list属性,您可以在其中传递要显示的电影列表。电影列表将被设置到适配器,更新用户界面中的RecyclerView

  5. Open the activity_main.xml file and wrap everything inside a layout tag:

    kt <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"> <androidx.constraintlayout.widget.ConstraintLayout ... > </androidx.constraintlayout.widget.ConstraintLayout> </layout>

    这样,数据绑定库将能够为此布局生成一个绑定类。

  6. Inside the layout tag and before the ConstraintLayout tag, add a data element with a variable for the viewModel:

    kt <data> <variable name="viewModel" type="com.example.popularmovies.MovieViewModel" /> </data>

    这将创建一个对应于您的MovieViewModel类的viewModel布局变量。

  7. In RecyclerView, add the list to be displayed with app:list:

    kt app:list="@{viewModel.popularMovies}"

    来自MovieViewModel.getPopularMoviesLiveData将作为RecyclerView的电影名单通过。

  8. Open MainActivity. In the onCreate function, replace the setContentView line with the following:

    kt val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)

    这将设置要使用的布局文件,并创建一个绑定对象。

  9. Replace the movieViewModel observer with the following:

    kt binding.viewModel = movieViewModel binding.lifecycleOwner = this

    这将movieViewModel绑定到activity_main.xml文件中的viewModel布局变量。

  10. 运行应用。它应该像往常一样工作,显示热门电影列表,点击其中一部将打开所选电影的详细信息:

Figure 14.2: The main screen (left) with the year’s popular movies sorted by title and the details screen (right) with more information about the selected movie

图 14.2:主屏幕(左)显示按标题排序的年度热门电影,详细信息屏幕(右)显示所选电影的更多信息

在本练习中,您已经在一个安卓项目中使用了数据绑定。

数据绑定将视图链接到视图模型。视图模型从模型中检索数据。您可以用来获取数据的一些库是 RetroFit 库和 Moshi 库,您将在下一节中详细了解它们。

RetroFit 和磨石

连接到远程网络时,您可以使用 RetroFit。RetroFit 是一个 HTTP 客户端,它使创建请求和从后端服务器检索响应变得容易。

您可以通过在app/build.gradle文件依赖项中添加以下代码来将 RetroFit 添加到您的项目中:

implementation 'com.squareup.retrofit2:retrofit:2.9.0'

然后,您可以通过使用 Moshi 将来自 Refusion 的 JSON 响应转换为 Java 对象,Moshi 是一个用于将 JSON 解析为 Java 对象的库。例如,您可以将获取电影列表的 JSON 字符串响应转换为ListofMovie对象,以便在您的应用中显示和存储。

您可以通过将以下代码添加到您的app/build.gradle文件依赖项来将 Moshi 转换器添加到您的项目中:

implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'

在您的 RetroFit 生成器代码中,您可以调用addConverterFactory并传递MoshiConverterFactory:

Retrofit.Builder()
    ...
    .addConverterFactory(MoshiConverterFactory.create())
    ...

您可以从视图模型调用数据层。为了降低复杂性,您可以使用存储库模式来加载和缓存数据。在下一节中,您将了解到这一点。

存储库模式

视图模型不是直接调用服务来获取和存储数据,而是应该将该任务委托给另一个组件,如存储库。

使用存储库模式,您可以将视图模型中处理数据层的代码移动到单独的类中。这降低了视图模型的复杂性,使其更容易维护和测试。存储库将管理数据的提取和存储位置,就像本地数据库或网络服务用于获取或存储数据一样:

Figure 14.3: ViewModel with the Repository pattern

图 14.3:带有存储库模式的视图模型

在视图模型中,您可以为存储库添加一个属性:

class MovieViewModel(val repository: MovieRepository): ViewModel() {
... 
}

视图模型将从存储库中获取电影,或者它可以收听它们。它不知道你到底是从哪里得到这份名单的。

您可以创建一个连接到数据源的存储库接口,如下例所示:

interface MovieRepository { 
    fun getMovies(): List<Movie>
}

MovieRepository接口有一个getMovies函数,您的存储库实现类将覆盖该函数以从数据源获取电影。您还可以有一个单独的存储库类来处理从本地数据库或远程端点获取数据。

当使用本地数据库作为存储库的数据源时,可以使用 Room 库,通过编写更少的代码和对查询进行编译时检查,可以更容易地使用 SQLite 数据库。

您可以通过将以下代码添加到您的app/build.gradle文件依赖项来将房间添加到您的项目中:

implementation 'androidx.room:room-runtime:2.2.5'
implementation 'androidx.room:room-ktx:2.2.5'
kapt 'androidx.room:room-compiler:2.2.5'

让我们尝试将带有房间的存储库模式添加到安卓项目中。

Exe rcise 14.02:在安卓项目中使用带房间的存储库

在上一个练习中,您已经在流行电影项目中添加了数据绑定。在本练习中,您将使用存储库模式更新应用。

打开应用时,它会从网络上获取电影列表。这需要一段时间。每次获取这些数据时,您都会将它们缓存到本地数据库中。当用户下次打开该应用时,该应用会立即在屏幕上显示数据库中的电影列表。您将使用空间进行数据缓存:

  1. 打开您在上一个练习中使用的Popular Movies项目。
  2. 打开app/build.gradle文件,添加房间库的依赖项:

    kt implementation 'androidx.room:room-runtime:2.2.5' implementation 'androidx.room:room-ktx:2.2.5' kapt 'androidx.room:room-compiler:2.2.5'

  3. Open the Movie class and add an Entity annotation for it:

    kt @Entity(tableName = "movies", primaryKeys = [("id")]) data class Movie( ... )

    Entity注释将为电影列表创建一个名为movies的表格。它还将id设置为表的主键。

  4. Make a new package called com.example.popularmovies.database. Create a MovieDao data access object for accessing the movies table:

    kt @Dao interface MovieDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun addMovies(movies: List<Movie>) @Query("SELECT * FROM movies") fun getMovies(): List<Movie> }

    这个类包含一个在数据库中添加电影列表的函数和一个从数据库中获取所有电影的函数。

  5. Create a MovieDatabase class in the com.example.popularmovies.database package:

    kt @Database(entities = [Movie::class], version = 1) abstract class MovieDatabase : RoomDatabase() { abstract fun movieDao(): MovieDao companion object { @Volatile private var instance: MovieDatabase? = null fun getInstance(context: Context): MovieDatabase { return instance ?: synchronized(this) { instance ?: buildDatabase(context).also { instance = it } } } private fun buildDatabase(context: Context): MovieDatabase { return Room.databaseBuilder(context, MovieDatabase::class.java, "movie-db") .build() } } }

    该数据库版本为 1,Movie为单个实体,电影为数据访问对象。它还有一个getInstance函数来生成数据库的一个实例。

  6. movieDatabase :

    kt class MovieRepository(private val movieService: MovieService, private val movieDatabase: MovieDatabase) { ... }

    的构造函数更新MovieRepository类 7. Update the fetchMovies function:

    kt suspend fun fetchMovies() { val movieDao: MovieDao = movieDatabase.movieDao() var moviesFetched = movieDao.getMovies() if (moviesFetched.isEmpty()) { try { val popularMovies = movieService.getPopularMovies(apiKey) moviesFetched = popularMovies.results movieDao.addMovies(moviesFetched) } catch (exception: Exception) { errorLiveData.postValue("An error occurred: ${exception.message}") } } movieLiveData.postValue(moviesFetched) }

    它将从数据库中获取电影。如果还没有保存任何内容,它将从网络端点检索列表,然后保存它。

  7. 打开MovieApplication,在onCreate功能中,用以下内容替换movieRepository初始化:

    kt val movieDatabase = MovieDatabase.getInstance(applicationContext) movieRepository = MovieRepository(movieService, movieDatabase)

  8. 运行应用。它将显示热门电影列表,点击其中一部将打开所选电影的详细信息。如果您关闭移动数据或断开与无线网络的连接,它仍会显示电影列表,该列表现在缓存在数据库中:

Figure 14.4: The Popular Movies app using Repository with Room

图 14.4:使用带房间的存储库的热门电影应用

在本练习中,您通过将数据的加载和存储移动到存储库中来改进应用。您还使用了“空间”来缓存数据。

存储库从数据源获取数据。如果数据库中还没有存储数据,应用将调用网络来请求数据。这可能需要一段时间。您可以通过在预定时间预取数据来改善用户体验,以便用户下次打开应用时,他们已经可以看到更新的内容。您可以使用工作管理器来实现这一点,我们将在下一节中讨论。

工人经理

WorkManager 是一个用于后台操作的 Jetpack 库,可以延迟,并且可以根据您设置的约束运行。它非常适合做一些必须运行但可以稍后或定期完成的事情,无论应用是否运行。

您可以使用工作管理器运行任务,例如从网络中获取数据,并按计划的时间间隔将其存储在数据库中。即使应用已关闭或设备重新启动,WorkManager 也会运行任务。这将使您的数据库与后端保持同步。

您可以通过将以下代码添加到您的app/build.gradle文件依赖项来将 WorkManager 添加到您的项目中:

implementation 'androidx.work:work-runtime:2.4.0'

工作管理器可以调用存储库从本地数据库或网络服务器获取和存储数据。

让我们尝试将工作管理器添加到安卓项目中。

练习练习 14.03:向安卓项目添加工作管理器

在前面的练习中,您添加了带有空间的存储库模式,以便在本地数据库中缓存数据。该应用现在可以从数据库而不是网络获取数据。现在,您将添加工作管理器来计划从服务器获取数据并按计划的时间间隔将其保存到数据库的任务:

  1. 打开您在上一个练习中使用的Popular Movies项目。
  2. Open the app/build.gradle file and add the dependency for the WorkManager library:

    kt implementation 'androidx.work:work-runtime:2.4.0'

    这将允许您向应用添加工作管理器工作人员。

  3. Open MovieRepository and add a suspending function for fetching movies from the network using the apiKey from The Movie Database, and saving them to the database:

    kt suspend fun fetchMoviesFromNetwork() { val movieDao: MovieDao = movieDatabase.movieDao() try { val popularMovies = movieService.getPopularMovies(apiKey) val moviesFetched = popularMovies.results movieDao.addMovies(moviesFetched) } catch (exception: Exception) { errorLiveData.postValue("An error occurred: ${exception.message}") } }

    这将是Worker类调用的函数,该类将运行以获取和保存电影。

  4. 创建MovieWorker类:

    kt class MovieWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) { override fun doWork(): Result { val movieRepository = (context as MovieApplication).movieRepository CoroutineScope(Dispatchers.IO).launch { movieRepository.fetchMoviesFromNetwork() } return Result.success() } }

  5. Open MovieApplication and at the end of the onCreate function, schedule MovieWorker to retrieve and save the movies:

    kt override fun onCreate() { ... val constraints = Constraints.Builder().setRequiredNetworkType( NetworkType.CONNECTED).build() val workRequest = PeriodicWorkRequest .Builder(MovieWorker::class.java, 1, TimeUnit.HOURS) .setConstraints(constraints) .addTag("movie-work") .build() WorkManager.getInstance(applicationContext).enqueue(workRequest) }

    这将安排MovieWorker在设备连接到网络时每小时运行一次。MovieWorker将从网络中获取电影列表并保存到本地数据库。

  6. 运行应用。关闭它,并确保设备已连接到互联网。一个多小时后,再次打开应用,检查显示的电影列表是否已更新。如果没有,几个小时后再试。即使应用已经关闭,显示的电影列表也会定期更新,大约每小时一次。

Figure 14.5: The Popular Movies app updates its list with WorkManager

图 14.5:热门电影应用使用工作管理器更新其列表

在本练习中,您将工作管理器添加到应用中,以使用从网络中检索到的电影列表自动更新数据库。

活动城市 14.01:重访电视指南应用

在前一章中,您开发了一个应用,可以显示正在播出的电视节目列表。这款应用有两个屏幕:主屏幕和细节屏幕。在主屏幕上,有一个电视节目列表。当点击一个电视节目时,详细信息屏幕将显示所选节目的详细信息。

运行应用时,需要一段时间才能显示节目列表。更新应用以缓存列表,以便在打开应用时立即显示该列表。此外,通过使用数据绑定的 MVVM 和添加工作管理器来改进应用。

您可以使用上一章中使用的电视指南应用,也可以从 GitHub 存储库中下载。以下步骤将帮助您完成本活动:

  1. 在 AndroidStudio 打开电视指南应用。打开app/build.gradle文件,添加kotlin-kapt插件、数据绑定依赖项以及房间和工作管理器的依赖项。
  2. RecyclerView创建一个绑定适配器类。
  3. activity_main.xml中,将所有内容包装在layout标签中。
  4. layout标签内部和ConstraintLayout标签之前,添加一个带有视图模型变量的数据元素。
  5. RecyclerView中,添加要用app:list显示的列表。
  6. MainActivity中,用DataBindingUtil.setContentView功能替换setContentView的线路。
  7. 用数据绑定代码替换TVShowViewModel开始的观察者。
  8. TVShow类中添加一个Entity标注。
  9. 创建TVDao数据访问对象,用于访问电视节目表。
  10. 创建一个TVDatabase类。
  11. tvDatabase的构造函数更新TVShowRepository
  12. 更新fetchTVShows功能,从本地数据库获取电视节目。如果那里还没有任何东西,从端点检索列表并将其保存在数据库中。
  13. 创建TVShowWorker类。
  14. 打开TVApplication文件。在onCreate中,安排TVShowWorker检索并保存节目。
  15. 运行您的应用。该应用将显示电视节目列表。单击电视节目将打开显示电影详细信息的详细信息活动。主屏幕和细节屏幕将类似于图 14.6 :

Figure 14.6: The main screen and details screen of the TV Guide app

图 14.6:电视指南应用的主屏幕和细节屏幕

注意

这个活动的解决方案可以在:http://packt.live/3sKj1cp找到

总结

他的章节集中在安卓的架构模式上。你从 MVVM 的建筑模式开始。您学习了它的三个组件:模型、视图和视图模型。您还使用数据绑定将视图与视图模型链接起来。

接下来,您学习了如何使用存储库模式来缓存数据。然后,您了解了 WorkManager,以及如何安排任务,例如从网络中检索数据并将数据保存到数据库以更新本地数据。

在下一章,你将学习如何用动画来改善你的应用的外观和设计。您将使用CoordinatorLayoutMotionLayout向您的应用添加动画和过渡。