简单的介绍Flutter的MVVM设计模式
在这篇文章中,我们将使用 MVVM 设计模式来编写一个完整的 Flutter 应用程序
默认情况下,Flutter 应用程序不使用任何特定的设计模式。
这意味着开发人员需要自己负责选择和实现适合他们需求的模式。
由于Flutter的声明性特性,使得其成为 MVVM 设计模式的理想候选者。
了解 MVVM
MVVM 代表模型-视图-视图模型。
基本思想是创建一个向视图提供数据的视图模型。视图可以使用视图模型提供的数据来填充自己。创建视图模型层允许您编写模块化代码,这些代码可以被多个视图使用。
MVVM 设计模式起源于微软。
MVVM 大量用于编写 Windows Presentation Foundation (WPF) 应用程序。 MVVM 模式是 PresentationModel 的一个花哨名称。
由于设计模式与平台无关,因此它可以与任何框架一起使用,包括 Flutter。
在这篇文章中,我们将创建一个电影应用程序,它将根据输入的关键字获取电影并将其显示给用户。这个应用程序将基于 MVVM 原则创建。
在深入研究代码之前,请查看下面的动画以了解我们将要构建的应用程序。
实现网络服务
我们将使用OMDb API来获取电影。确保您在他们的网站上注册以获取 API 密钥。如果没有有效的 API 密钥,您将无法成功执行请求。
现在您已经注册了 OMDb 服务并收到了您的密钥,我们可以继续开发 Web 服务/客户端。 Web 服务使用 HTTP 包发出网络请求并下载 JSON 响应,但您可以随意使用任何您想要的网络包。
下载 JSON 后,将其灌入到电影模型以获取电影对象列表,如下所示:
import 'package:movies_app/models/movie.dart';
import 'package:http/http.dart' as http;
class Webservice {
Future<List<Movie>> fetchMovies(String keyword) async {
final url = "http://www.omdbapi.com/?s=$keyword&apikey=YOURAPIKEYHERE";
final response = await http.get(url);
if(response.statusCode == 200) {
final body = jsonDecode(response.body);
final Iterable json = body["Search"];
return json.map((movie) => Movie.fromJson(movie)).toList();
} else {
throw Exception("Unable to perform request!");
}
}
}
电影模型实现如下:
class Movie {
final String title;
final String poster;
Movie({this.title, this.poster});
factory Movie.fromJson(Map<String, dynamic> json) {
return Movie(
title: json["Title"],
poster: json["Poster"]
);
}
}
电影只是由标题和海报组成。它还公开了一个 fromJson 函数,它允许我们基于返回的JSON格式数据创建一个 Movie 对象。
我们使用模型来定义我们的电影对象,但实际上,它们是用来实现为数据传输对象 (DTO) 的服务的。
至此,我们拥有了所有需要的数据,下一步就是将其显示在屏幕上。在将数据显示在用户界面之前,我们必须创建负责向视图提供数据的视图模型。
实现视图模型
尽管您可以通过创建单个视图模型来获得所需的结果,但我们将在我们的应用程序中创建两个单独的视图模型。
一个视图模型 MoviesListViewModel 将用来表示电影列表数据并显示在整个屏幕上。
第二个视图模型 MovieViewModel 将表示一个单独的电影——它将显示在视图中。
视图模型的实现如下所示:
class MovieListViewModel extends ChangeNotifier {
List<MovieViewModel> movies = List<MovieViewModel>();
Future<void> fetchMovies(String keyword) async {
// TODO
}
}
class MovieViewModel {
final Movie movie;
MovieViewModel({this.movie});
String get title {
return this.movie.title;
}
String get poster {
return this.movie.poster;
}
}
MovieListViewModel 由 movies 属性组成,该属性将返回 MovieViewModel 对象的列表。 MovieViewModel 接受一个 Movie 对象并将标题和海报作为只读属性返回。下一步是使用 Web 服务获取电影。
设置 ChangeNotifier 和 ChangeNotifierProvider
MovieListViewModel 中的 fetchMovies 方法会通过使用Web服务,从 OMDb API 检索电影。
实现如下图所示:
class MovieListViewModel extends ChangeNotifier {
List<MovieViewModel> movies = List<MovieViewModel>();
Future<void> fetchMovies(String keyword) async {
final results = await Webservice().fetchMovies(keyword);
this.movies = results.map((item) => MovieViewModel(movie: item)).toList();
print(this.movies);
notifyListeners();
}
}
我们更新了 MovieListViewModel 以从 ChangeNotifier 继承。 ChangeNotifier 允许我们发布更改通知,视图可以使用它来更新自身。
在我们使用网络服务获取电影后,我们调用 notifyListeners 函数,通知所有订阅者/听众。目前没有人收听,所以没有人得到消息:电影已经下载完毕。
为了用更新的 MovieListViewModel 通知视图,我们必须使用 ChangeNotifierProvider,它是 Provider 包的一部分。通过在pubspec.yaml文件中添加依赖来添加provider包,如下图:
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
http: ^0.12.0+2
provider: ^3.2.0
接下来,我们需要找到一个从提供者那里注入值的好地方。
我们正在谈论的值是 MovieListViewModel 的一个实例,因为它扩展了 ChangeNotifier 并向侦听器发布通知。
在我们的例子中,我们可以使用 main.dart 文件并将值注入到 MovieListPage。这意味着 MovieListViewModel 将可用于 MovieListPage 及其所有子项。
void main() => runApp(App());
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Movies",
home:
ChangeNotifierProvider(
create: (context) => MovieListViewModel(),
child: MovieListPage(),
)
);
}
}
完美!最后一步是在屏幕上显示数据。
显示电影
对于我们的应用程序,我们将要求用户输入关键字并按键盘上的搜索键。这将调用 MovieListViewModel 上的 fetchMovies 方法,如下所示:
Container(
padding: EdgeInsets.only(left: 10),
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(10)
),
child: TextField(
controller: _controller,
onSubmitted: (value) {
if(value.isNotEmpty) {
vm.fetchMovies(value);
_controller.clear();
}
},
fetchMovies 将根据关键字获取所有电影并触发 notifyListeners。为了获取 MovieListViewModel 的更新实例,我们将从provider包中获得帮助,如下所示:
@override Widget build(BuildContext context) {
final vm = Provider.of<MovieListViewModel>(context);
通过在 build 方法中添加 Provider,我们确保无论何时触发 notifyListener,我们都可以访问 MovieListViewModel 的实例。
现在,如果您运行您的应用程序,它将根据关键字获取电影并将其显示在用户界面上。
如果您想在页面加载时执行初始提取,则可以将 StatelessWidget 更新为 StatefulWidget 并从 initState 方法内部调用 fetchMovies,如下所示:
class _MovieListPageState extends State<MovieListPage> {
final TextEditingController _controller = TextEditingController();
@override
void initState() {
super.initState();
// you can uncomment this to get all batman movies when the page is loaded
Provider.of<MovieListViewModel>(context, listen: false).fetchMovies("batman");
}
请注意,我们为 Provider 传递了 listen: false ,这意味着这只是一次调用,并且 Provider 不会跟踪更改。
以上所有源代码,您可以移步:https://github.com/reasonpun/my_100_goals/tree/main/goals_02
谢谢阅读!