Flutter 中的 MVVM 实现

3,941 阅读2分钟

概述

其实本身 flutter 就是一个 mvvm 的架构, 当你加载完数据 ,直接 setState() 就可以了,

MVVM各代表什么

  • M:Model class,便是指这里的Repository ,主要负责从本地数据库或者远程服务器来获取数据,Repository统一了数据的入口,获取到数据,将数据发送给 ViewModel
  • VM:ViewModel ,即 Flutter 中的ValueNotifier 或者是 ChangeNotifier
  • V:View ,即 Flutter 中 Widget,也可以认为 ValueListenableBuilder 或者是 ChangeNotifierProvider

使用MVVM实现一个简单的登录页面

比如我们去请求一个网络的书籍

先建 Model

class BookModel {
 
  String? name; // 书的名字
  String? author;// 书的作者
  String? detail;

  int loadState = 1; // 0 : 加载中  1:加载成功 2:加载失败
}

创建我们的BookRepository

下面的是模拟网络去请求数据,用一个变量 当 count 是偶数的时候模拟网络错误,是奇数的时候模拟网络成功

class BookRepository {
  var count = 0;
  Future<BookModel> getData() async {
    await Future.delayed(const Duration(seconds: 2));
    BookModel model = BookModel();
    count++;
    if (count.isOdd) {
      throw Exception("网络错误");
    } else {
      model.name = "Flutter$count";
      model.author = "google$count";
    }

    return model;
  }
}

再写ViewModel层

class BookViewModel extends ValueNotifier<BookModel> {
 // 创建 BookRepository
  BookRepository repository = BookRepository();

  BookViewModel() : super(BookModel());
  
  // 当 已经 dispose 的时候,就不要在发了
  bool _dispose = false;

  @override
  void dispose() {
    super.dispose();
    _dispose = true;
  }

  @override
  void notifyListeners() {
    if (!_dispose) {
      super.notifyListeners();
    }
  }

 /// 用来请求数据
  void getData() {
   // 这里的value 就是 BookModel
   // 设置为加载中
    value.loadState = 0;
    // 通知改变
    notifyListeners();
    repository.getData().then((book) {
     // 设置加载成功
      book.loadState = 1;
      // 赋值的时候 会调用notifyListeners
      value = book;
    }).catchError((e) {
     // 设置网络错误的码
      value.loadState = 2;
      notifyListeners();
    });
  }
}

创建我们的View层

下面有使用 ValueListenableBuilder 以及 ChangeNotifierProvider 两种方式去实现,具体请看代码

class FlutterBook extends StatefulWidget {
  FlutterBook({Key? key}) : super(key: key);

  @override
  _FlutterBookState createState() => _FlutterBookState();
}

class _FlutterBookState extends State<FlutterBook> {
  final BookViewModel _viewModel = BookViewModel();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('MVVM的demo'),
      ),
      // body: _useValueListenableBuilderBody(),
      body: _useProviderBody(),
    );
  }

  /// 使用 ValueListenableBuilder
  Widget _useValueListenableBuilder() {
    return ValueListenableBuilder<BookModel>(
      valueListenable: _viewModel,
      builder: (BuildContext context, BookModel model, Widget? child) {
        return _bodyChild(model);
      },
    );
  }
  /// 使用 ChangeNotifierProvider
  Widget _useProviderBody() {
    return ChangeNotifierProvider(
      create: (_) => _viewModel,
      child: Consumer<BookViewModel>(
          builder: (context, BookViewModel viewModel, child) {
        BookModel model = viewModel.value;
        return _bodyChild(model);
      }),
    );
  }

  Widget _bodyChild(BookModel model) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Container(
          alignment: Alignment.center,
          width: 200,
          height: 200,
          child: IndexedStack(
            alignment: Alignment.center,
            // 根据加载状态去显示对应的布局
            index: model.loadState,
            children: [
              const CircularProgressIndicator(),
              Text(
                "名字是:${model.name ?? ""} , 作者是:${model.author ?? ""}",
              ),
              // 加载失败,点击重试
              InkWell(
                child: Image.asset("assets/img/net_error.jpg"),
                onTap: _viewModel.getData,
              )
            ],
          ),
        ),
        Container(
            margin:
                const EdgeInsets.only(top: 50, left: 50, right: 50, bottom: 0),
            height: 50,
            width: MediaQuery.of(context).size.width,
            child: MaterialButton(
              elevation: 3,
              shape: const RoundedRectangleBorder(
                  borderRadius: BorderRadius.all(Radius.circular(20))),
              color: Theme.of(context).primaryColor,
              minWidth: 60,
               // 点击加载
              onPressed: _viewModel.getData,
              child: const Text("请求数据",
                  style: TextStyle(color: Colors.white, fontSize: 20)),
            )),
      ],
    );
  }
}

效果

1628243569549934.gif