Flutter中的MVVM设计模式

10

我们尝试开发一个Flutter应用,并创建一个有状态的小部件作为页面。
我们希望将build函数与其他状态变量和状态函数分开放置在两个不同的文件中,以便build函数可以访问到状态类的this。 我们创建了一个类:

PageClassState extend State<PageClass>{
    string value = 'string value';
}

扩展它并在一个新类中访问PageClassStatethis变量,我们编写:

PageClassView extend PageClassState{
    @override
    Widget Build(){
      return(new Text(this.value))
    }
} 

但在 PageClassState 中,我们会收到一个错误提示,说我们必须重写该类中的 build 方法。是否有任何建议可以解决该问题并在 Flutter 中实现 MVVM 设计模式?


你可以使用Scoped Model。 - Soohwan Park
5个回答

17

我建议将ViewModel代码移动到一个不扩展 State 的单独类中。保持ViewModel的平台独立性。 您的小部件状态可以具有viewModel的实例并与其交互。

您可以在此处找到更详细的示例

如果子小部件需要访问ViewModel,您可以像 @Rémi Rousselet 建议的那样使用Inherited Widget。 我为您快速实现了这个:

class ViewModelProvider extends InheritedWidget {
  final ViewModel viewModel;

  ViewModelProvider({Key key, @required this.viewModel, Widget child}) 
  : super(key: key, child: child);

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => true;

  static ViewModel of(BuildContext context) =>
      (context.inheritFromWidgetOfExactType(ViewModelProvider) as 
  ViewModelProvider).viewModel;
}

子组件可以通过调用此方法来获取ViewModel

var viewModel = ViewModelProvider.of(context);

如果你有任何问题,请告诉我 :)


4
如何处理具有50多个ViewModel的应用程序? - Hemant Kaushik

9

那不是正确的方法。你不应该拆分 State<T> 和它的 build 方法。事实上,不要扩展widget,而是组合它们。

实现类似功能的正确方式是使用 InheritedWidget。这些Widget将保存您的数据但不执行任何其他操作。其子Widget可以使用 MyInherited.of(context) 请求这些数据。

您也可以创建一个 builder,像这样:

typedef Widget MyStateBuilder(BuildContext context, MyStateState state);

class MyState extends StatefulWidget {
  final MyStateState builder;

  const MyState({this.builder}) : assert(builder != null);

  @override
  MyStateState createState() => new MyStateState();
}

class MyStateState extends State<MyState> {
  String name;

  @override
  Widget build(BuildContext context) {
    return widget.builder(context, this);
  }
}

2

我一直在使用这个插件来维护Flutter的大型应用程序。mvvm_flutter

 https://pub.dev/packages/mvvm_flutter

它非常轻巧易用,可以查看一些示例。它非常容易维护UI,远离业务逻辑。


0

请参考我的 Github https://github.com/anandh-ps/flutter_mvvm_example

。这与编程有关。

MediaService.dart

import 'dart:convert';
import 'dart:io';
import 'package:meta/meta.dart';

import 'package:http/http.dart' as http;
import 'package:mvvm_flutter_app/model/apis/app_exception.dart';

class MediaService {
  final String _baseUrl = "https://itunes.apple.com/search?term=";

  Future<dynamic> get(String url) async {
    dynamic responseJson;
    try {
      final response = await http.get(_baseUrl + url);
      responseJson = returnResponse(response);
    } on SocketException {
      throw FetchDataException('No Internet Connection');
    }
    return responseJson;
  }

  @visibleForTesting
  dynamic returnResponse(http.Response response) {
    switch (response.statusCode) {
      case 200:
        dynamic responseJson = jsonDecode(response.body);
        return responseJson;
      case 400:
        throw BadRequestException(response.body.toString());
      case 401:
      case 403:
        throw UnauthorisedException(response.body.toString());
      case 500:
      default:
        throw FetchDataException(
            'Error occured while communication with server' +
                ' with status code : ${response.statusCode}');
    }
  }
}

MediaRepository.dart

import 'package:mvvm_flutter_app/model/media.dart';
import 'package:mvvm_flutter_app/model/services/media_service.dart';

class MediaRepository {
  MediaService _mediaService = MediaService();

  Future<List<Media>> fetchMediaList(String value) async {
    dynamic response = await _mediaService.get(value);
    final jsonData = response['results'] as List;
    List<Media> mediaList =
        jsonData.map((tagJson) => Media.fromJson(tagJson)).toList();
    return mediaList;
  }
}

MediaViewModel.dart

import 'package:flutter/cupertino.dart';
import 'package:mvvm_flutter_app/model/apis/api_response.dart';
import 'package:mvvm_flutter_app/model/media.dart';
import 'package:mvvm_flutter_app/model/media_repository.dart';

class MediaViewModel with ChangeNotifier {
  ApiResponse _apiResponse = ApiResponse.loading('Fetching artist data');

  Media _media;

  ApiResponse get response {
    return _apiResponse;
  }

  Media get media {
    return _media;
  }

  /// Call the media service and gets the data of requested media data of
  /// an artist.
  Future<void> fetchMediaData(String value) async {
    try {
      List<Media> mediaList = await MediaRepository().fetchMediaList(value);
      _apiResponse = ApiResponse.completed(mediaList);
    } catch (e) {
      _apiResponse = ApiResponse.error(e.toString());
      print(e);
    }
    notifyListeners();
  }

  void setSelectedMedia(Media media) {
    _media = media;
    notifyListeners();
  }
}

HomeScreen.dart

import 'package:flutter/material.dart';
import 'package:mvvm_flutter_app/model/apis/api_response.dart';
import 'package:mvvm_flutter_app/model/media.dart';
import 'package:mvvm_flutter_app/view/widgets/player_list_widget.dart';
import 'package:mvvm_flutter_app/view/widgets/player_widget.dart';
import 'package:mvvm_flutter_app/view_model/media_view_model.dart';

import 'package:provider/provider.dart';

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final _inputController = TextEditingController();
    ApiResponse apiResponse = Provider.of<MediaViewModel>(context).response;
    List<Media> mediaList = apiResponse.data as List<Media>;
    return Scaffold(
        appBar: AppBar(
          title: Text('Media Player'),
        ),
        body: Column(
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 10.0),
              child: Row(
                children: <Widget>[
                  Expanded(
                    child: Container(
                      margin: EdgeInsets.symmetric(horizontal: 20.0),
                      decoration: BoxDecoration(
                        color: Theme.of(context).accentColor.withAlpha(50),
                        borderRadius: BorderRadius.circular(30.0),
                      ),
                      child: TextField(
                          style: TextStyle(
                            fontSize: 15.0,
                            color: Colors.grey,
                          ),
                          controller: _inputController,
                          onChanged: (value) {},
                          onSubmitted: (value) {
                            if (value.isNotEmpty) {
                              Provider.of<MediaViewModel>(context)
                                  .setSelectedMedia(null);
                              Provider.of<MediaViewModel>(context,
                                      listen: false)
                                  .fetchMediaData(value);
                            }
                          },
                          decoration: InputDecoration(
                            border: InputBorder.none,
                            enabledBorder: InputBorder.none,
                            focusedBorder: InputBorder.none,
                            prefixIcon: Icon(
                              Icons.search,
                              color: Colors.grey,
                            ),
                            hintText: 'Enter Artist Name',
                          )),
                    ),
                  ),
                ],
              ),
            ),
            mediaList != null && mediaList.length > 0
                ? Expanded(
                    child: PlayerListWidget(mediaList, (Media media) {
                    Provider.of<MediaViewModel>(context)
                        .setSelectedMedia(media);
                  }))
                : Expanded(
                    child: Center(
                      child: Text('Search the song by Artist'),
                    ),
                  ),
            if (Provider.of<MediaViewModel>(context).media != null)
              Align(
                  alignment: Alignment.bottomCenter,
                  child: PlayerWidget(
                    function: () {
                      setState(() {});
                    },
                  )),
          ],
        ));
  }
}

0

MVVM包,是Flutter的MVVM(Model-View-ViewModel)实现。

import 'package:flutter/widgets.dart';
import 'package:mvvm/mvvm.dart';
import 'dart:async';

// ViewModel
class Demo1ViewModel extends ViewModel {

  Demo1ViewModel() {
      // define bindable property
      propertyValue<String>(#time, initial: "");
      // timer
      start();
  }

  start() {
      Timer.periodic(const Duration(seconds: 1), (_) {
        var now = DateTime.now();
        // call setValue
        setValue<String>(#time, "${now.hour}:${now.minute}:${now.second}");
      });
  }
}

// View
class Demo1 extends View<Demo1ViewModel> {
  Demo1() : super(Demo1ViewModel());

  @override
  Widget buildCore(BuildContext context) {
    return Container(
        margin: EdgeInsets.symmetric(vertical: 100),
        padding: EdgeInsets.all(40),

        // binding
        child: $.watchFor(#time, 
            builder: $.builder1((t) => 
              Text(t, textDirection: TextDirection.ltr))));
  }
}

// run
void main() => runApp(Demo1());

完整示例


我认为你的库文档已经过时了,因为当我尝试检查一些方法时会出错。 - DolDurma

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接