Browse Source

update example

ditclear 7 years ago
parent
commit
01bfeb8a8d

BIN
device-2019-03-08-164030.png


+ 38 - 0
lib/di/modules.dart

@@ -0,0 +1,38 @@
+import 'dart:convert';
+
+import 'package:dio/dio.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:mvvm_flutter/model/remote.dart';
+import 'package:mvvm_flutter/model/repository.dart';
+import 'package:mvvm_flutter/view/home.dart';
+import 'package:provide/provide.dart';
+
+
+final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
+
+String token="";
+
+final Dio dio=Dio()
+  ..options = BaseOptions(
+      baseUrl: 'https://api.github.com/',
+      connectTimeout: 30,
+      receiveTimeout: 30)
+  ..interceptors.add(AuthInterceptor())
+  ..interceptors.add(LogInterceptor(responseBody: true, requestBody: true));
+
+GithubService _provideGithubService()=>GithubService();
+GithubRepo _provideGithubRepo()=>GithubRepo(_provideGithubService());
+HomeViewModel provideHomeViewModel() => HomeViewModel(_provideGithubRepo());
+
+
+final providers = Providers();
+
+class AuthInterceptor extends Interceptor{
+
+  @override
+  onRequest(RequestOptions options) {
+    options.headers.update("Authorization",(_)=>token,ifAbsent: ()=>token);
+    return super.onRequest(options);
+  }
+}

+ 21 - 0
lib/helper/netutils.dart

@@ -0,0 +1,21 @@
+
+import 'dart:async';
+
+import 'package:mvvm_flutter/di/modules.dart';
+import 'package:rxdart/rxdart.dart';
+
+Future _get(String url, {Map<String, dynamic> params}) async {
+  var response = await dio.get(url, queryParameters: params);
+  return response.data;
+}
+
+Observable post(String url, Map<String, dynamic> params) =>
+    Observable.fromFuture(_post(url, params)).asBroadcastStream();
+
+Observable get(String url, {Map<String, dynamic> params}) =>
+    Observable.fromFuture(_get(url, params: params)).asBroadcastStream();
+
+Future _post(String url, Map<String, dynamic> params) async {
+  var response = await dio.post(url, data: params);
+  return response.data;
+}

+ 134 - 0
lib/helper/toast.dart

@@ -0,0 +1,134 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+
+class Toast {
+  static const int LENGTH_SHORT = 1;
+  static const int LENGTH_LONG = 2;
+  static const int BOTTOM = 0;
+  static const int CENTER = 1;
+  static const int TOP = 2;
+  static const int ERROR = -1;
+  static const int NORMAL = 0;
+  static const int SUCCESS = 1;
+
+  static void show(String msg, BuildContext context,
+      {int type = NORMAL,
+      int duration = 1,
+      int gravity = CENTER,
+      Color backgroundColor = const Color(0xAA000000),
+      Color textColor = Colors.white,
+      double backgroundRadius = 5.0}) {
+    ToastView.dismiss();
+    ToastView.createView(msg, context, type, duration, gravity, backgroundColor,
+        textColor, backgroundRadius);
+  }
+}
+
+class ToastView {
+  static final ToastView _singleton = new ToastView._internal();
+
+  factory ToastView() {
+    return _singleton;
+  }
+
+  ToastView._internal();
+
+  static OverlayState overlayState;
+  static OverlayEntry overlayEntry;
+  static bool _isVisible = false;
+
+  static void createView(
+      String msg,
+      BuildContext context,
+      int type,
+      int duration,
+      int gravity,
+      Color background,
+      Color textColor,
+      double backgroundRadius) async {
+    overlayState = Overlay.of(context);
+    overlayEntry = new OverlayEntry(
+      builder: (BuildContext context) => ToastWidget(
+          widget: Container(
+            width: MediaQuery.of(context).size.width,
+            child: Container(
+              alignment: Alignment.center,
+              child: Container(
+                margin: EdgeInsets.symmetric(horizontal: 83),
+                padding: EdgeInsets.all(15.0),
+                decoration: BoxDecoration(
+                  color: background,
+                  borderRadius: BorderRadius.circular(backgroundRadius),
+                ),
+                constraints: BoxConstraints(minHeight: 52,minWidth: 210),
+                child: _buildContent(type, msg, textColor),
+              ),
+            ),
+          ),
+          gravity: gravity),
+    );
+    _isVisible = true;
+    overlayState.insert(overlayEntry);
+    await new Future.delayed(
+        Duration(seconds: duration == null ? Toast.LENGTH_SHORT : duration));
+    dismiss();
+  }
+
+  static Widget _buildContent(int type, String msg, Color textColor) {
+    if (type == 0) {
+      return Text(msg,
+          maxLines: 20,
+          overflow: TextOverflow.ellipsis,
+          textAlign: TextAlign.center,
+          style: TextStyle(
+              fontSize: 16,
+              color: textColor,
+              decoration: TextDecoration.none,
+              fontWeight: FontWeight.normal));
+    } else {
+      return Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        mainAxisSize: MainAxisSize.min,
+        children: <Widget>[
+          Icon(type ==1 ?Icons.check_circle:Icons.error,color: Colors.white,),
+          Padding(padding: EdgeInsets.only(top: 16.0),),
+          Text(msg,
+              maxLines: 20,
+              overflow: TextOverflow.ellipsis,
+              style: TextStyle(
+                  fontSize: 16,
+                  color: textColor,
+                  decoration: TextDecoration.none,
+                  fontWeight: FontWeight.normal))
+        ],
+      );
+    }
+  }
+
+  static dismiss() async {
+    if (!_isVisible) {
+      return;
+    }
+    _isVisible = false;
+    overlayEntry?.remove();
+  }
+}
+
+class ToastWidget extends StatelessWidget {
+  ToastWidget({
+    Key key,
+    @required this.widget,
+    @required this.gravity,
+  }) : super(key: key);
+
+  final Widget widget;
+  final int gravity;
+
+  @override
+  Widget build(BuildContext context) {
+    return new Positioned(
+        top: gravity == 2 ? 50 : null,
+        bottom: gravity == 0 ? 50 : null,
+        child: widget);
+  }
+}

+ 30 - 0
lib/helper/widgetutils.dart

@@ -0,0 +1,30 @@
+import 'dart:io';
+
+import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
+import 'package:mvvm_flutter/helper/toast.dart';
+
+dispatchFailure(BuildContext context, dynamic e) {
+  var message = e.toString();
+  if (e is DioError) {
+    final response = e.response;
+
+    if (response?.statusCode == 401) {
+      message = "账号或密码错误";
+    } else if (403 == response?.statusCode) {
+      message = "禁止访问";
+    } else if (404 == response?.statusCode) {
+      message = "链接错误";
+    } else if (500 == response?.statusCode) {
+      message = "服务器内部错误";
+    } else if (503 == response?.statusCode) {
+      message = "服务器升级中";
+    } else if (e.error is SocketException) {
+      message = "网络未连接";
+    } else {
+      message = "Oops!!";
+    }
+  }
+  print("出错了:"+message);
+  Toast.show(message, context, type: Toast.ERROR);
+}

+ 12 - 96
lib/main.dart

@@ -1,15 +1,23 @@
 import 'package:flutter/material.dart';
+import 'package:mvvm_flutter/di/modules.dart';
+import 'package:mvvm_flutter/model/remote.dart';
+import 'package:mvvm_flutter/model/repository.dart';
+import 'package:mvvm_flutter/view/home.dart';
 import 'package:provide/provide.dart';
 
-void main() => runApp(ProviderNode(
-    child: MyApp(), providers: Providers()..provideValue(CountViewModel(0))));
+void main() {
+
+  runApp(ProviderNode(child: MyApp(), providers: providers));
+
+}
 
 class MyApp extends StatelessWidget {
   // This widget is the root of your application.
+
   @override
   Widget build(BuildContext context) {
     return MaterialApp(
-      title: 'Flutter Demo',
+      title: 'MVVM-Flutter',
       theme: ThemeData(
         // This is the theme of your application.
         //
@@ -22,99 +30,7 @@ class MyApp extends StatelessWidget {
         // is not restarted.
         primarySwatch: Colors.blue,
       ),
-      home: MyHomePage(title: 'Flutter Demo Home Page'),
-    );
-  }
-}
-
-class MyHomePage extends StatefulWidget {
-  MyHomePage({Key key, this.title}) : super(key: key);
-
-  // This widget is the home page of your application. It is stateful, meaning
-  // that it has a State object (defined below) that contains fields that affect
-  // how it looks.
-
-  // This class is the configuration for the state. It holds the values (in this
-  // case the title) provided by the parent (in this case the App widget) and
-  // used by the build method of the State. Fields in a Widget subclass are
-  // always marked "final".
-
-  final String title;
-
-  @override
-  _MyHomePageState createState() => _MyHomePageState();
-}
-
-class _MyHomePageState extends State<MyHomePage> {
-  @override
-  Widget build(BuildContext context) {
-    // This method is rerun every time setState is called, for instance as done
-    // by the _incrementCounter method above.
-    //
-    // The Flutter framework has been optimized to make rerunning build methods
-    // fast, so that you can just rebuild anything that needs updating rather
-    // than having to individually change instances of widgets.
-    return Scaffold(
-      appBar: AppBar(
-        // Here we take the value from the MyHomePage object that was created by
-        // the App.build method, and use it to set our appbar title.
-        title: Text(widget.title),
-      ),
-      body: Center(
-        // Center is a layout widget. It takes a single child and positions it
-        // in the middle of the parent.
-        child: Column(
-          // Column is also layout widget. It takes a list of children and
-          // arranges them vertically. By default, it sizes itself to fit its
-          // children horizontally, and tries to be as tall as its parent.
-          //
-          // Invoke "debug painting" (press "p" in the console, choose the
-          // "Toggle Debug Paint" action from the Flutter Inspector in Android
-          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
-          // to see the wireframe for each widget.
-          //
-          // Column has various properties to control how it sizes itself and
-          // how it positions its children. Here we use mainAxisAlignment to
-          // center the children vertically; the main axis here is the vertical
-          // axis because Columns are vertical (the cross axis would be
-          // horizontal).
-          mainAxisAlignment: MainAxisAlignment.center,
-          children: <Widget>[
-            const Text(
-              'You have pushed the button this many times:',
-            ),
-            Provide<CountViewModel>(
-              builder:
-                  (BuildContext context, Widget child, CountViewModel value) =>
-                  Text('${value.value}',
-                    style: Theme.of(context).textTheme.display1,
-                  ),
-            ),
-          ],
-        ),
-      ),
-      floatingActionButton: Provide<CountViewModel>(
-        builder: (BuildContext context, Widget child,CountViewModel value) =>
-            FloatingActionButton(
-              onPressed: value.increment,
-              tooltip: 'Increment',
-              child: Icon(Icons.add),
-            ),
-      ), // This trailing comma makes auto-formatting nicer for build methods.
+      home: HomeWidget(),
     );
   }
 }
-
-class CountViewModel extends ChangeNotifier {
-  int _value;
-
-  int get value => _value;
-
-  CountViewModel(this._value);
-
-  void increment() {
-    _value++;
-    notifyListeners();
-  }
-
-}

+ 8 - 0
lib/model/remote.dart

@@ -0,0 +1,8 @@
+
+import 'package:rxdart/rxdart.dart';
+import 'package:mvvm_flutter/helper/netutils.dart';
+class GithubService{
+
+  Observable<dynamic> login()=> get("user");
+
+}

+ 16 - 0
lib/model/repository.dart

@@ -0,0 +1,16 @@
+import 'dart:convert';
+
+import 'package:mvvm_flutter/di/modules.dart';
+import 'package:mvvm_flutter/model/remote.dart';
+import 'package:rxdart/rxdart.dart';
+
+class GithubRepo {
+  final GithubService _remote;
+
+  GithubRepo(this._remote);
+
+  Observable login(String username, String password) {
+    token = "basic " + base64Encode(utf8.encode('$username:$password'));
+    return _remote.login();
+  }
+}

+ 221 - 0
lib/view/home.dart

@@ -0,0 +1,221 @@
+import 'package:dio/dio.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:mvvm_flutter/di/modules.dart';
+import 'package:mvvm_flutter/helper/toast.dart';
+import 'package:mvvm_flutter/helper/widgetutils.dart';
+import 'package:mvvm_flutter/model/repository.dart';
+import 'package:provide/provide.dart';
+import 'package:rxdart/rxdart.dart';
+
+class HomeWidget extends StatefulWidget {
+  @override
+  State<StatefulWidget> createState() {
+    return _HomeState(provideHomeViewModel());
+  }
+}
+
+/**
+ * View
+ */
+class _HomeState extends State<HomeWidget>
+    with SingleTickerProviderStateMixin<HomeWidget> {
+  final HomeViewModel _viewModel;
+  final CompositeSubscription _subscriptions = CompositeSubscription();
+
+  AnimationController _controller;
+  Animation<double> _animation;
+
+  _HomeState(this._viewModel) {
+    providers.provideValue(_viewModel);
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = AnimationController(
+        vsync: this, duration: const Duration(milliseconds: 300));
+    _animation = Tween(begin: _viewModel.btnWidth, end: 48.0).animate(_controller)
+      ..addListener(() {
+        _viewModel.btnWidth = _animation.value;
+      });
+  }
+
+  _login() {
+    final s = _viewModel.login().doOnListen(() {
+      _controller.forward();
+    }).doOnDone(() {
+      _controller.reverse();
+    }).listen((_) {
+      //success
+      Toast.show("login success",context,type: Toast.SUCCESS);
+    }, onError: (e) {
+      //error
+      dispatchFailure(context, e);
+    });
+    _subscriptions.add(s);
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+    _subscriptions.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text("MVVM-Flutter"),
+      ),
+      body: Material(
+        child: Column(
+          children: <Widget>[
+            TextField(
+              keyboardType: TextInputType.text,
+              decoration: InputDecoration(
+                contentPadding: EdgeInsets.all(10.0),
+                icon: Icon(Icons.person),
+                labelText: '账号',
+              ),
+              autofocus: false,
+              onChanged: (str) => _viewModel.username = str,
+            ),
+            TextField(
+              obscureText: true,
+              keyboardType: TextInputType.number,
+              decoration: InputDecoration(
+                contentPadding: EdgeInsets.all(10.0),
+                icon: Icon(Icons.lock),
+                labelText: '密码',
+              ),
+              autofocus: false,
+              onChanged: (str) => _viewModel.password = str,
+            ),
+            const Padding(
+              padding: EdgeInsets.only(top: 30.0),
+            ),
+            Provide<HomeViewModel>(
+              builder: (BuildContext context, Widget child,
+                      HomeViewModel value) =>
+                  CupertinoButton(
+                    onPressed: value.loading?null:_login,
+                    pressedOpacity: 0.8,
+                    child: Container(
+                      alignment: Alignment.center,
+                      width: value.btnWidth,
+                      height: 48,
+                      decoration: BoxDecoration(
+                          borderRadius: BorderRadius.all(Radius.circular(30.0)),
+                          gradient: LinearGradient(colors: [
+                            Color(0xFF686CF2),
+                            Color(0xFF0E5CFF),
+                          ]),
+                          boxShadow: [
+                            BoxShadow(
+                                color: Color(0x4D5E56FF),
+                                offset: Offset(0.0, 4.0),
+                                blurRadius: 13.0)
+                          ]),
+                      child: _buildChild(value),
+                    ),
+                  ),
+            ),
+            const Text(
+              "Response:",
+              style: TextStyle(fontSize: 18),
+              textAlign: TextAlign.start,
+            ),
+            Expanded(
+              child: Container(
+                constraints: BoxConstraints(minWidth: double.infinity),
+                margin: EdgeInsets.fromLTRB(12, 12, 12, 0),
+                padding: EdgeInsets.all(5.0),
+                decoration:
+                    BoxDecoration(border: Border.all(color: Colors.blue)),
+                child: Provide<HomeViewModel>(
+                  builder: (BuildContext context, Widget child,
+                          HomeViewModel value) =>
+                      Text(value.response),
+                ),
+              ),
+            )
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildChild(HomeViewModel value) {
+    if (value.loading) {
+      return const CircularProgressIndicator();
+    } else {
+      return const FittedBox(
+        fit: BoxFit.scaleDown,
+        child: const Text(
+          '使用GitHub账号登录',
+          maxLines: 1,
+          textAlign: TextAlign.center,
+          overflow: TextOverflow.fade,
+          style: const TextStyle(
+              fontWeight: FontWeight.bold, fontSize: 16.0, color: Colors.white),
+        ),
+      );
+    }
+  }
+}
+
+/**
+ * ViewModel
+ */
+class HomeViewModel extends ChangeNotifier {
+  final GithubRepo _repo; //数据仓库
+  String username = ""; //账号
+  String password = ""; //密码
+  bool _loading = false; // 加载中
+  String _response = ""; //响应数据
+
+  String get response => _response;
+
+  set response(String response) {
+    _response = response;
+    notifyListeners();
+  }
+
+  bool get loading => _loading;
+
+  double _btnWidth = 295.0;
+
+  double get btnWidth => _btnWidth;
+
+  set btnWidth(double btnWidth) {
+    _btnWidth = btnWidth;
+    notifyListeners();
+  }
+
+  set loading(bool loading) {
+    _loading = loading;
+    notifyListeners();
+  }
+
+  HomeViewModel(this._repo);
+
+  /**
+   * 调用model层的方法进行登录
+   * doOnData : 请求成功时,处理响应数据
+   * doOnError : 请求失败时,处理错误
+   * doOnListen : 开始时loading为true,通知ui更新
+   * doOnDone : 结束时loading为false,通知ui更新
+   */
+  Observable login() => _repo
+      .login(username, password)
+      .doOnData((r) => response = r.toString())
+      .doOnError((e, stacktrace) {
+        if (e is DioError) {
+          response = e.response.data.toString();
+        }
+      })
+      .doOnListen(() => loading = true)
+      .doOnDone(() => loading = false);
+}

+ 5 - 0
pubspec.yaml

@@ -20,11 +20,16 @@ dependencies:
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^0.1.2
   provide: ^1.0.2
+  dio: 2.0.7
+  json_annotation: ^2.0.0
+  rxdart: ^0.21.0
 
 dev_dependencies:
   flutter_test:
     sdk: flutter
 
+  build_runner: ^1.0.0
+  json_serializable: ^2.0.0
 
 # For information on the generic Dart part of this file, see the
 # following page: https://www.dartlang.org/tools/pub/pubspec