从0开始写一个基于Flutter的开源中国客户端(8)——插件的使用_void parseresult(future<string-程序员宅基地

技术标签: Google Flutter  

上一篇中我记录了基于Flutter的开源中国客户端里网络请求和数据存储的部分,本篇记录的是app中插件的使用,由于很多功能并没有内置到Flutter中,所以我们需要引入一些插件来帮助我们完成某些功能,比如app内网页的加载,图库选择照片等。

搜索插件包

要使用插件,必须知道插件叫什么名字,目前是什么版本,Flutter提供了一个插件仓库,可以去上面搜索相关的插件,仓库地址为:https://pub.dartlang.org/,但是这个网站在国内可能访问不了,国内可以用Flutter专门为中国开发者提供的网站:https://pub.flutter-io.cn/。该网站打开后直接在输入框中搜索关键字即可,如下图所示:

比如我们需要在app中用WebView加载网页,可以直接搜索’web view’,再或者我们需要调用图库选择图片的功能,可以搜索’image picker’,搜索结果可能有一大堆,怎么选择合适的插件呢?

由于我们是开发Flutter应用,所以要在搜索结果中过滤出供Flutter使用的插件,如下图所示:

过滤是第一步,过滤之后,还要查看插件包的更新日期,更新日期不能是很久前,因为很早之前发布的插件包,可能并不适合现在的Flutter版本,另外就是看这个插件后面的数字,数字越大表示插件匹配程度越高,如下图所示:

上面两步过滤之后,选择你觉得合适的插件,点进去看看详情,里面有相关的插件说明,示例用法,确定可以完成你所需要的功能,就可以愉快的在项目中添加插件依赖了。

基本上每个插件的主页都会有说明如何在项目中添加该插件的依赖,比如在我们这个基于Flutter的开源中国客户端中,用到了flutter_webview_plugin这个插件,在该插件的主页里,就有怎么引入依赖的说明:

使用flutter_webview_plugin插件

在基于Flutter的开源中国客户端项目中,用户登录和资讯详情等页面都使用了WebView加载网页,使用的是flutter_webview_plugin这个插件。该插件主要功能是可以在Flutter页面中加载一个WebView,并且可以监听WebView的各种状态比如加载中,加载完成等,而且还能读取WebView中的cookies,或者通过dart代码调用WebView中的js方法。

开源中国提供的基于oauth的认证流程大致如下:
1. 在开源中国后台添加应用,完善应用的信息,最主要的是回调地址,该地址将会在后面用到;
2. 使用浏览器或者WebView加载三方认证页面,在该页面中输入开源中国的用户名和密码(输入密码的页面为开源中国提供的页面,第三方是无法获取密码信息的);
3. 输入用户名和密码后点击页面上的登录按钮,若登录成功,将会跳转到第一步我们在后台配置的回调地址上,并给该页面传入一个code参数(code参数直接拼接在URL上);
4. 在该页面中接收code参数,并根据开源中国后台提供的client_id client_secret等参数换取token信息(这一步就是一个get请求,只不过放在我自己的服务端进行了);
5. 上面的请求成功后,开源中国的openapi会返回token等信息,在我们的回调页面将这个信息通过js的一个get()方法暴露出来,让dart代码去调用。

具体的oauth认证流程可以查看开源中国的文档:文档地址

构造登录页面

lib/pages/目录下新建LoginPage.dart文件,并使用flutter_webview_plugin插件提供的WebviewScaffold组件,该组件会在页面上渲染一个WebView用于加载某个URL,代码如下:

  @override
  Widget build(BuildContext context) {
    List<Widget> titleContent = [];
    titleContent.add(new Text(
      "登录开源中国",
      style: new TextStyle(color: Colors.white),
    ));
    if (loading) {
      // 如果还在加载中,就在标题栏上显示一个圆形进度条
      titleContent.add(new CupertinoActivityIndicator());
    }
    titleContent.add(new Container(width: 50.0));
    // WebviewScaffold是插件提供的组件,用于在页面上显示一个WebView并加载URL
    return new WebviewScaffold(
      key: _scaffoldKey,
      url: Constants.LOGIN_URL, // 登录的URL
      appBar: new AppBar(
        title: new Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: titleContent,
        ),
        iconTheme: new IconThemeData(color: Colors.white),
      ),
      withZoom: true,  // 允许网页缩放
      withLocalStorage: true, // 允许LocalStorage
      withJavascript: true, // 允许执行js代码
    );
  }

上面的代码中,我们给AppBar组件上加了标题,还加了一个圆形的进度条,用于指示WebView加载的状态,如果在加载中,就显示进度条,否则就隐藏进度条(所以LoginPage类应该继承StatefulWidget)。

监听WebView的加载状态和URL变化

flutter_webview_plugin插件提供的api可以监听WebView加载的状态和URL的变化,主要代码如下:

// 登录页面,使用网页加载的开源中国三方登录页面
class LoginPage extends StatefulWidget {
    
  @override
  State<StatefulWidget> createState() => new LoginPageState();
}

class LoginPageState extends State<LoginPage> {
    
  // 标记是否是加载中
  bool loading = true;
  // 标记当前页面是否是我们自定义的回调页面
  bool isLoadingCallbackPage = false;
  GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey();
  // URL变化监听器
  StreamSubscription<String> _onUrlChanged;
  // WebView加载状态变化监听器
  StreamSubscription<WebViewStateChanged> _onStateChanged;
  // 插件提供的对象,该对象用于WebView的各种操作
  FlutterWebviewPlugin flutterWebViewPlugin = new FlutterWebviewPlugin();

  @override
  void initState() {
    super.initState();
    // 监听WebView的加载事件,该监听器已不起作用,不回调
    _onStateChanged = flutterWebViewPlugin.onStateChanged.listen((WebViewStateChanged state) {
      // state.type是一个枚举类型,取值有:WebViewState.shouldStart, WebViewState.startLoad, WebViewState.finishLoad
      switch (state.type) {
        case WebViewState.shouldStart:
          // 准备加载
          setState(() {
            loading = true;
          });
          break;
        case WebViewState.startLoad:
          // 开始加载
          break;
        case WebViewState.finishLoad:
          // 加载完成
          setState(() {
            loading = false;
          });
          if (isLoadingCallbackPage) {
            // 当前是回调页面,则调用js方法获取数据
            parseResult();
          }
          break;
      }
    });
    _onUrlChanged = flutterWebViewPlugin.onUrlChanged.listen((url) {
      // 登录成功会跳转到自定义的回调页面,该页面地址为http://yubo725.top/osc/osc.php?code=xxx
      // 该页面会接收code,然后根据code换取AccessToken,并将获取到的token及其他信息,通过js的get()方法返回
      if (url != null && url.length > 0 && url.contains("osc/osc.php?code=")) {
        isLoadingCallbackPage = true;
      }
    });
  }
}

上面代码的逻辑是:

  • 监听WebView的加载状态,控制loading的改变达到改变AppBar上进度条的目的;
  • 监听页面URL的改变,若页面URL中包含“osc/osc.php?code=”,代表开源中国的账号密码验证通过,并跳转到了我们自定义的回调页面,这里给isLoadingCallbackPage赋值为true,代表当前加载的是回调页面;
  • 在WebView的WebViewState.finishLoad状态中,判断如果当前页是回调页,则可以通过parseResult()方法调用js代码获取token信息了。

dart调用js代码获取token信息

parseResult()方法中就是dart调用js代码的逻辑了,flutter_webview_plugin插件提供了API供我们很方便的用dart代码调用js代码,下面是parseResult()方法的代码:

  // 解析WebView中的数据
  void parseResult() {
    flutterWebViewPlugin.evalJavascript("get();").then((result) {
      // result json字符串,包含token信息
      if (result != null && result.length > 0) {
        // 拿到了js中的数据
        try {
          // what the fuck?? need twice decode??
          var map = json.decode(result); // s is String
          if (map is String) {
            map = json.decode(map); // map is Map
          }
          if (map != null) {
            // 登录成功,取到了token,关闭当前页面
            DataUtils.saveLoginInfo(map);
            Navigator.pop(context, "refresh");
          }
        } catch (e) {
          print("parse login result error: $e");
        }
      }
    });
  }

主要方法是flutterWebViewPlugin.evalJavascript()传入的参数是一个字符串,表示要执行的js代码。上面的代码意思是执行页面中的get()方法,在该方法中返回了token等信息,然后在then中解析这些信息,并调用DataUtils.saveLoginInfo(map);保存登录信息,这就到了上一篇中我记录的数据保存的部分了。数据保存后调用Navigator.pop(context, "refresh");方法将当前页推出栈,后面的”refresh”参数有什么作用呢?

通知上一个页面登录成功,让上一个页面刷新

“refresh”的作用就是为了让上一个页面刷新(这里只是一个字符串参数,定义成什么样子完全取决于你自己)。如果是做过Android开发的朋友,应该会很熟悉,我们要把当前页的数据传递给上一个页面,一般会在上一个页面用startActivityForResult方法启动当前页,上一个页面会在onActivityResult回调方法中接收参数。Flutter的做法跟这个有点类似,在“我的”页面中打开登录页时,使用下面的方法:

  _login() async {
    // 打开登录页并处理登录成功的回调
    final result = await Navigator
        .of(context)
        .push(new MaterialPageRoute(builder: (context) {
      return new LoginPage();
    }));
    // result为"refresh"代表登录成功
    if (result != null && result == "refresh") {
      // 刷新用户信息
      getUserInfo();
      // 通知动弹页面刷新
      Constants.eventBus.fire(new LoginEvent());
    }
  }

上面的代码应该很明了了吧,Navigatorpush方法返回的是一个Future对象,所以我们可以在then里面处理登录页返回的信息,登录页pop时传入的’refresh’字符串,将会在这里被接收,接收到就可以刷新“我的”页面了(刷新用户昵称和头像)。

使用event_bus插件

上面最后的_login()方法的代码中,我们收到了”refresh”参数后,获取并刷新了页面的用户信息,然后还调用了一行代码用于刷新动弹页面:

Constants.eventBus.fire(new LoginEvent());

这行代码就用到了另外一个框架:event_bus

如果做过Android开发或者前端开发,应该对这个框架不陌生。EventBus是一个发布/订阅模式的框架,用于在某个页面订阅某个事件,然后在另外的地方触发这个事件,订阅这个事件的方法就会被执行。

该框架在pub仓库的主页是:https://pub.flutter-io.cn/packages/event_bus

该插件的用法很简单,首先是导入包:

import 'package:event_bus/event_bus.dart';

如果要订阅某个事件,使用下面的代码:

new EventBus().on(MyEvent).listen((event) {
    // 处理事件
});

其中MyEvent是自定义的一个类,表示唯一的一个事件。如果要监听所有的事件,on方法中可以不传参数。

要发送某个事件,可以用如下代码:

new EventBus().fire(new MyEvent());

使用fire方法发送某个事件,参数就是这个自定义的事件对象,可以在这个对象中加入任何你需要的参数。

在基于Flutter的开源中国客户端项目中,可以只用到一个EventBus对象,没必要在每次用的时候都new EventBus(),所以我们在lib/constants/Constants.dart中定义了一个静态的eventBus变量,全局都可以共用这一个对象:

static EventBus eventBus = new EventBus();

在登录成功后,调用如下代码来通知动弹列表刷新:

Constants.eventBus.fire(new LoginEvent());

LoginEvent是一个空的类,表示登录成功的事件。

在动弹列表页,还要为登录成功的事件加上监听:

Constants.eventBus.on(LoginEvent).listen((event) {
  setState(() {
    this.isUserLogin = true;
  });
});

动弹列表页根据上面的isUserLogin变量加载不同的页面,如果该变量为false,表示当前没有登录,则显示如下界面:

如果该变量为true,则会调用开源中国的api去获取动弹信息,显示如下界面:

关于动弹列表的加载,这里就不详细说明了,文末会给出源码链接。

使用image_picker插件

在发送动弹的页面,有选择图片的功能,如下图所示:

Flutter并没有提供相关API供我们操作移动设备的图库,所以这里又用到了image_picker插件,该插件的地址在这里:https://pub.flutter-io.cn/packages/image_picker

导入插件的代码如下:

import 'package:image_picker/image_picker.dart';

插件的使用方法也比较简单,如下代码:

// source是一个枚举值,可取值有ImageSource.camera和ImageSource.gallery,分别代表调用相机和图库
_imageFile = ImagePicker.pickImage(source: source);

显示底部弹出菜单

上图中的弹出菜单在Flutter中已有内置的组件可直接使,当我们点击选择图片时,调用pickImage方法,代码如下:

  // 相机拍照或者从图库选择图片
  pickImage(ctx) {
    // 如果已添加了9张图片,则提示不允许添加更多
    num size = fileList.length;
    if (size >= 9) {
      Scaffold.of(ctx).showSnackBar(new SnackBar(
        content: new Text("最多只能添加9张图片!"),
      ));
      return;
    }
    // Flutter提供的API,用于显示一个底部弹出的Dialog
    showModalBottomSheet<void>(context: context, builder: _bottomSheetBuilder);
  }

  // 自定义底部菜单的布局
  Widget _bottomSheetBuilder(BuildContext context) {
    return new Container(
      height: 182.0,
      child: new Padding(
        padding: const EdgeInsets.fromLTRB(0.0, 30.0, 0.0, 30.0),
        child: new Column(
          children: <Widget>[
            _renderBottomMenuItem("相机拍照", ImageSource.camera),
            new Divider(height: 2.0,),
            _renderBottomMenuItem("图库选择照片", ImageSource.gallery)
          ],
        ),
      )
    );
  }

  // 渲染底部菜单的每个item
  _renderBottomMenuItem(title, ImageSource source) {
    var item = new Container(
      height: 60.0,
      child: new Center(
        child: new Text(title)
      ),
    );
    return new InkWell(
      child: item,
      onTap: () { 
        // 点击菜单item,关闭这个底部弹窗并调用相机或者图库
        Navigator.of(context).pop();
        setState(() {
          _imageFile = ImagePicker.pickImage(source: source);
        });
      },
    );
  }

上面代码中的_imageFile是一个Future<File>对象,因为选择图片的操作是异步的,那么在什么地方接收选择的图片呢?不论是拍照还是图库选择,最后调用ImagePicker.pickImage(source: source)返回的都是一个文件对象,在image_picker主页给出的示例代码中,是以组件的形式返回一个FutureBuilder<File>对象,在该对象的builder方法中接收返回的图片文件的。

在基于Flutter的开源中国客户端项目中,接收选择的图片是放在build方法中的,PublishTweetPage页面的build方法代码如下:

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("发布动弹", style: new TextStyle(color: Colors.white)),
        iconTheme: new IconThemeData(color: Colors.white),
        actions: <Widget>[
          new Builder(
            builder: (ctx) {
              return new IconButton(icon: new Icon(Icons.send), onPressed: () {
                // 发送动弹
                DataUtils.isLogin().then((isLogin) {
                  if (isLogin) {
                    return DataUtils.getAccessToken();
                  } else {
                    return null;
                  }
                }).then((token) {
                  sendTweet(ctx, token);
                });
              });
            },
          )
        ],
      ),
      // 在这里接收选择的图片
      body: new FutureBuilder(
        future: _imageFile,
        builder: (BuildContext context, AsyncSnapshot<File> snapshot) {
          if (snapshot.connectionState == ConnectionState.done &&
              snapshot.data != null && _imageFile != null) {
            // 选择了图片(拍照或图库选择),添加到List中
            fileList.add(snapshot.data);
            _imageFile = null;
          }
          // 返回的widget
          return getBody();
        },
      ),
    );
  }

在AppBar的右边添加了一个按钮,用于发送动弹信息。在body部分返回了一个FutureBuilder对象,在该对象的builder方法中接收了选中的图片文件,并将该文件加入到图片列表中,然后调用getBody()方法返回整个页面,这么做的原因是因为每次选中一张图片后,都需要将页面刷新,在getBody()方法中会用到fileList变量,getBody()方法代码如下:

  Widget getBody() {
    // 输入框
    var textField = new TextField(
      decoration: new InputDecoration(
        hintText: "说点什么吧~",
        hintStyle: new TextStyle(
          color: const Color(0xFF808080)
        ),
        border: new OutlineInputBorder(
          borderRadius: const BorderRadius.all(const Radius.circular(10.0))
        )
      ),
      // 最多显示6行文本(不代表最多只能输入6行)
      maxLines: 6,
      // 最多输入的文字数
      maxLength: 150,
      // 通过_controller.text可以获取输入框中输入的文本
      controller: _controller,
    );
    // gridView用来显示选择的图片
    var gridView = new Builder(
      builder: (ctx) {
        return new GridView.count(
          // 分4列显示
          crossAxisCount: 4,
          children: new List.generate(fileList.length + 1, (index) {
            // 这个方法体用于生成GridView中的一个item
            var content;
            if (index == 0) {
              // 添加图片按钮
              var addCell = new Center(
                  child: new Image.asset('./images/ic_add_pics.png', width: 80.0, height: 80.0,)
              );
              content = new GestureDetector(
                onTap: () {
                  // 添加图片
                  pickImage(ctx);
                },
                child: addCell,
              );
            } else {
              // 被选中的图片
              content = new Center(
                  child: new Image.file(fileList[index - 1], width: 80.0, height: 80.0, fit: BoxFit.cover,)
              );
            }
            return new Container(
              margin: const EdgeInsets.all(2.0),
              width: 80.0,
              height: 80.0,
              color: const Color(0xFFECECEC),
              child: content,
            );
          }),
        );
      },
    );
    var children = [
      new Text("提示:由于OSC的openapi限制,发布动弹的接口只支持上传一张图片,本项目可添加最多9张图片,但OSC只会接收最后一张图片。", style: new TextStyle(fontSize: 12.0),),
      textField,
      new Container(
          margin: const EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 0.0),
          height: 200.0,
          child: gridView
      )
    ];
    if (isLoading) { // 上传图片可能会比较慢,所以这里显示loading
      children.add(new Container(
        margin: const EdgeInsets.fromLTRB(0.0, 20.0, 0.0, 0.0),
        child: new Center(
          child: new CircularProgressIndicator(),
        ),
      ));
    } else { // 上传成功后显示msg
      children.add(new Container(
        margin: const EdgeInsets.fromLTRB(0.0, 20.0, 0.0, 0.0),
        child: new Center(
          child: new Text(msg),
        )
      ));
    }
    return new Container(
      padding: const EdgeInsets.all(5.0),
      child: new Column(
        children: children,
      ),
    );
  }

获取到了选择的图片和输入的动弹内容,下一步是发送动弹,发送动弹调用的是开源中国的openapi,这里涉及到使用dart上传图片的问题,下面先上代码:

  sendTweet(ctx, token) async {
    // 未登录或者未输入动弹内容时,使用SnackBar提示用户
    if (token == null) {
      Scaffold.of(ctx).showSnackBar(new SnackBar(
        content: new Text("未登录!"),
      ));
      return;
    }
    String content = _controller.text;
    if (content == null || content.length == 0 || content.trim().length == 0) {
      Scaffold.of(ctx).showSnackBar(new SnackBar(
        content: new Text("请输入动弹内容!"),
      ));
    }
    // 下面是调用接口发布动弹的逻辑
    try {
      Map<String, String> params = new Map();
      params['msg'] = content;
      params['access_token'] = token;
      // 构造一个MultipartRequest对象用于上传图片
      var request = new MultipartRequest('POST', Uri.parse(Api.PUB_TWEET));
      request.fields.addAll(params);
      if (fileList != null && fileList.length > 0) {
        // 这里虽然是添加了多个图片文件,但是开源中国提供的接口只接收一张图片
        for (File f in fileList) {
          // 文件流
          var stream = new http.ByteStream(
              DelegatingStream.typed(f.openRead()));
          // 文件长度
          var length = await f.length();
          // 文件名
          var filename = f.path.substring(f.path.lastIndexOf("/") + 1);
          // 将文件加入到请求体中
          request.files.add(new http.MultipartFile(
              'img', stream, length, filename: filename));
        }
      }
      setState(() {
        isLoading = true;
      });
      // 发送请求
      var response = await request.send();
      // 解析请求返回的数据
      response.stream.transform(utf8.decoder).listen((value) {
        print(value);
        if (value != null) {
          var obj = json.decode(value);
          var error = obj['error'];
          setState(() {
            if (error != null && error == '200') {
              // 成功
              setState(() {
                isLoading = false;
                msg = "发布成功";
                fileList.clear();
              });
              _controller.clear();
            } else {
              setState(() {
                isLoading = false;
                msg = "发布失败:$error";
              });
            }
          });
        }
      });
    } catch (exception) {
      print(exception);
    }
  }

使用dart上传图片的代码和普通的get/post请求是完全不一样的,上传图片需要构造一个Request对象:

var request = new MultipartRequest('POST', Uri.parse(Api.PUB_TWEET));

添加普通的参数需要调用request.field.addAll方法:

request.fields.addAll(params); // params是参数map

添加文件参数时,需要调用request.files.add方法:

request.files.add(new http.MultipartFile(
    'img', stream, length, filename: filename));

解析返回的数据时需要使用如下代码:

  // 发送请求
  var response = await request.send();
  // 解析请求返回的数据
  response.stream.transform(utf8.decoder).listen((value) {})

关于发送动弹的详细代码,可以参考文末的源码链接,这里不再说明。

源码

本篇相关的所有源码都在GitHub上flutter-osc项目

后记

  • 本篇主要记录的是基于Flutter的开源中国客户端app中的各种插件的使用。

  • 二维码扫描的插件使用在本篇中没有做记录,各位小伙伴可自行上pub仓库搜索插件用法。

  • 本系列博客并未将所有功能的实现方法都记录下来,只是有选择性的记录了一部分功能的实现。

  • 本项目中还有很多功能暂未实现,比如动弹大图预览、个人信息页的展示等。大部分的功能都是以WebView的形式加载的,所以整体来看app的实现并不复杂,代码量也并不多,开源出来希望给学习Flutter的小伙伴们一点帮助。(如果对你有帮助,请在github给个start支持一下��)

  • 本项目中还有一些已知和未知的bug,已知的bug是token过期后没有做自动刷新处理(开源中国给的token是有有效期的,过期后需要使用refresh_token去刷新access_token),未知的一些bug可能会导致app在运行过程中ANR,由于没有对各个机型做测试,所以暂时不知道ANR是什么原因导致的,但是在开发过程中会偶现插件的报错,希望各位发现bug可以及时与我联系(文末留言或者github提issue都行),感谢你们的支持!

我的开源项目

  1. 基于Google Flutter的开源中国客户端,希望大家给个Star支持一下,源码:

  1. 基于Flutter的俄罗斯方块小游戏,希望大家给个Star支持一下,源码:

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/yubo_725/article/details/81661553

智能推荐

苹果https java_apple登录 后端java实现最终版-程序员宅基地

文章浏览阅读298次。import com.alibaba.fastjson.JSONArray;import com.alibaba.fastjson.JSONObject;import com.auth0.jwk.Jwk;import com.helijia.appuser.modules.user.vo.AppleCredential;import com.helijia.common.api.model.Api..._com.auth0.jwk.jwk

NLP学习记录(六)最大熵模型MaxEnt_顺序潜在最大熵强化学习(maxent rl)-程序员宅基地

文章浏览阅读4.7k次。原理在叧掌握关于未知分布的部分信息的情况下,符合已知知识的概率分布可能有夗个,但使熵值最大的概率分布最真实地反映了事件的的分布情况,因为熵定义了随机变量的不确定性,弼熵值最大时,随机变量最不确定,最难预测其行为。最大熵模型介绍我们通过一个简单的例子来介绍最大熵概念。假设我们模拟一个翻译专家的决策过程,关于英文单词in到法语单词的翻译。我们的翻译决策模型p给每一个单词或短语分配一..._顺序潜在最大熵强化学习(maxent rl)

计算机毕业设计ssm科研成果管理系统p57gs系统+程序+源码+lw+远程部署-程序员宅基地

文章浏览阅读107次。计算机毕业设计ssm科研成果管理系统p57gs系统+程序+源码+lw+远程部署。springboot基于springboot的影视资讯管理系统。ssm基于SSM高校教师个人主页网站的设计与实现。ssm基于JAVA的求职招聘网站的设计与实现。springboot校园头条新闻管理系统。ssm基于SSM框架的毕业生离校管理系统。ssm预装箱式净水站可视化信息管理系统。ssm基于SSM的网络饮品销售管理系统。

Caused by: org.xml.sax.SAXParseException; lineNumber: 38; columnNumber: 9; cvc-complex-type.2.3: 元素_saxparseexception; linenumber: 35; columnnumber: 9-程序员宅基地

文章浏览阅读1.6w次。不知道大家有没有遇到过与我类似的报错情况,今天发生了此错误后就黏贴复制了报错信息“Caused by: org.xml.sax.SAXParseException; lineNumber: 38; columnNumber: 9; cvc-complex-type.2.3: 元素 'beans' 必须不含字符 [子级], 因为该类型的内容类型为“仅元素”。”然后就是一顿的百度啊, 可一直都没有找到..._saxparseexception; linenumber: 35; columnnumber: 9; cvc-complex-type.2.3:

计算机科学与技术创新创业意见,计算机科学与技术学院大学生创新创业工作会议成功举行...-程序员宅基地

文章浏览阅读156次。(通讯员 粟坤萍 2018-04-19)4月19日,湖北师范大学计算机科学与技术学院于教育大楼学院会议室1110成功召开大学生创新创业工作会议。参与本次会议的人员有党总支副书记黄海军老师,创新创业学院吴杉老师,计算机科学与技术学院创新创业活动指导老师,15、16、17级各班班主任及学生代表。首先吴杉老师介绍了“互联网+”全国大学生创新创业大赛的相关工作进度,动员各级班主任充分做好“大学生创新创业大..._湖北师范 吴杉

【Android逆向】爬虫进阶实战应用必知必会-程序员宅基地

文章浏览阅读1.1w次,点赞69次,收藏76次。安卓逆向技术是一门深奥且充满挑战的领域。通过本文的介绍,我们了解了安卓逆向的基本概念、常用工具、进阶技术以及实战案例分析。然而,逆向工程的世界仍然在不断发展和变化,新的技术和方法不断涌现。展望未来,随着安卓系统的不断更新和加固,逆向工程将面临更大的挑战。同时,随着人工智能和机器学习技术的发展,我们也许能够看到更智能、更高效的逆向工具和方法的出现。由于篇幅限制,本文仅对安卓逆向技术进行了介绍和案例分析。

随便推点

Python数据可视化之环形饼图_数据可视化绘制饼图或圆环图-程序员宅基地

文章浏览阅读1.1k次。制作饼图还需要下载pyecharts库,Echarts 是一个由百度开源的数据可视化,凭借着良好的交互性,精巧的图表设计,得到了众多开发者的认可。随着学习python的热潮不断增加,Python数据可视化也不停的被使用,那我今天就介绍一下Python数据可视化中的饼图。在我们的生活和学习中,编程是一项非常有用的技能,能够丰富我们的视野,为各行各业的领域提供了新的角度。环形饼图的制作并不难,主要是在于数据的打包和分组这里会有点问题,属性的标签可以去 这个网站进行修改。图中的zip压缩函数,并分组打包。_数据可视化绘制饼图或圆环图

SpringMVC开发技术~5~基于注解的控制器_jsp/servlet到controller到基于注解的控制器-程序员宅基地

文章浏览阅读325次。1 Spring MVC注解类型Controller和RequestMapping注释类型是SpringMVC API最重要的两个注释类型。基于注解的控制器的几个优点:一个控制器类可以控制几个动作,而一个实现了Controller接口的控制器只能处理一个动作。这就允许将相关操作写在一个控制器类内,从而减少应用类的数量基于注解的控制器的请求映射不需要存储在配置文件中,而是使用RequestM..._jsp/servlet到controller到基于注解的控制器

利用波特图来满足动态控制行为的要求-程序员宅基地

文章浏览阅读260次,点赞3次,收藏4次。相位裕量可以从增益图中的交越频率处读取(参见图2)。使用的开关频率、选择的外部元件(例如电感和输出电容),以及各自的工作条件(例如输入电压、输出电压和负载电流)都会产生巨大影响。图2所示为波特图中控制环路的增益曲线,其中提供了两条重要信息。对于图2所示的控制环路,这个所谓的交越频率出现在约80 kHz处。通过使用波特图,您可以查看控制环路的速度,特别是其调节稳定性。图2. 显示控制环路增益的波特图(约80 kHz时,达到0 dB交越点)。图3. 控制环路的相位曲线,相位裕量为60°。

Glibc Error: `_obstack@GLIBC_2.2.5‘ can‘t be versioned to common symbol ‘_obstack_compat‘_`_obstack@glibc_2.2.5' can't be versioned to commo-程序员宅基地

文章浏览阅读1.8k次。Error: `_obstack@GLIBC_2.2.5’ can’t be versioned to common symbol '_obstack_compat’原因:https://www.lordaro.co.uk/posts/2018-08-26-compiling-glibc.htmlThis was another issue relating to the newer binutils install. Turns out that all was needed was to initi_`_obstack@glibc_2.2.5' can't be versioned to common symbol '_obstack_compat

基于javaweb+mysql的电影院售票购票电影票管理系统(前台、后台)_电影售票系统javaweb-程序员宅基地

文章浏览阅读3k次。基于javaweb+mysql的电影院售票购票电影票管理系统(前台、后台)运行环境Java≥8、MySQL≥5.7开发工具eclipse/idea/myeclipse/sts等均可配置运行适用课程设计,大作业,毕业设计,项目练习,学习演示等功能说明前台用户:查看电影列表、查看排版、选座购票、查看个人信息后台管理员:管理电影排版,活动,会员,退票,影院,统计等前台:后台:技术框架_电影售票系统javaweb

分分钟拯救监控知识体系-程序员宅基地

文章浏览阅读95次。分分钟拯救监控知识体系本文出自:http://liangweilinux.blog.51cto.com0 监控目标我们先来了解什么是监控,监控的重要性以及监控的目标,当然每个人所在的行业不同、公司不同、业务不同、岗位不同、对监控的理解也不同,但是我们需要注意,监控是需要站在公司的业务角度去考虑,而不是针对某个监控技术的使用。监控目标1.对系统不间断实时监控:实际上是对系统不间..._不属于监控目标范畴的是 实时反馈系统当前状态

推荐文章

热门文章

相关标签