zoukankan      html  css  js  c++  java
  • Flutter项目实操---我的界面搭建、开源中国API流程了解、HTTP请求封装、登录处理<二>

    我的界面搭建:

    继续接着上一次https://www.cnblogs.com/webor2006/p/12879031.html的代码继续编写,这次则会使用到开源中国的API的调用了,所以说怎么在Flutter中来进行网络访问的技能通过这次就能够GET到的,这里先从用户登录开始,因为开源中国的API都依赖于AccessToken,所以先来搭建一下我的界面,目前我的界面是一个空壳:

    而最终要的效果是这样:

    下面先来搭建界面:

    菜单数据准备:

    先定义元素文本及图标:

    import 'package:flutter/material.dart';
    
    class ProfilePage extends StatefulWidget {
      @override
      _ProfilePageState createState() => _ProfilePageState();
    }
    
    class _ProfilePageState extends State<ProfilePage> {
      List menuTitles = [
        '我的消息',
        '阅读记录',
        '我的博客',
        '我的问答',
        '我的活动',
        '我的团队',
        '邀请好友',
      ];
      List menuIcons = [
        Icons.message,
        Icons.print,
        Icons.error,
        Icons.phone,
        Icons.send,
        Icons.people,
        Icons.person,
      ];
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Text('我的'),
        );
      }
    }

    构建菜单项:

    这里肯定是用ListView来构建了,这块之前学习过,直接上代码:

    import 'package:flutter/material.dart';
    
    class ProfilePage extends StatefulWidget {
      @override
      _ProfilePageState createState() => _ProfilePageState();
    }
    
    class _ProfilePageState extends State<ProfilePage> {
      List menuTitles = [
        '我的消息',
        '阅读记录',
        '我的博客',
        '我的问答',
        '我的活动',
        '我的团队',
        '邀请好友',
      ];
      List menuIcons = [
        Icons.message,
        Icons.print,
        Icons.error,
        Icons.phone,
        Icons.send,
        Icons.people,
        Icons.person,
      ];
    
      @override
      Widget build(BuildContext context) {
        return ListView.separated(
            itemBuilder: (context, index) {
              if (index == 0) {
                return Container(
                  height: 200.0,
                  color: Colors.red,
                );
              }
              index -= 1;
              return ListTile(
                leading: Icon(menuIcons[index]), //左图标
                title: Text(menuTitles[index]), //中间标题
                trailing: Icon(Icons.arrow_forward_ios),
              );
            },
            separatorBuilder: (context, index) {
              return Divider();
            }, //分隔线
            itemCount: menuTitles.length + 1);
      }
    } 

    上面由于将头像也做为列表的一项,所以在总列表项个数是+1,比较好理解,先看一下运行效果:

    是不是感觉Flutter构建UI代码还是挺少的,像模像样的列表就出来了,接下来则来构建用户头像:

    @override
      Widget build(BuildContext context) {
        return ListView.separated(
            itemBuilder: (context, index) {
              if (index == 0) {
                //用户头像
                return Center(
                  child: Column(
                    children: <Widget>[
                      Container(
                         60.0,
                        height: 60.0,
                        decoration: BoxDecoration(
                            shape: BoxShape.circle,
                            border: Border.all(
                              color: Colors.white,
                               2.0,
                            ),
                            image: DecorationImage(
                              image:
                                  AssetImage('assets/images/ic_avatar_default.png'),
                              fit: BoxFit.cover,
                            )),
                      )
                    ],
                  ),
                );
              }
              index -= 1;
              return ListTile(
                leading: Icon(menuIcons[index]), //左图标
                title: Text(menuTitles[index]), //中间标题
                trailing: Icon(Icons.arrow_forward_ios),
              );
            },
            separatorBuilder: (context, index) {
              return Divider();
            }, //分隔线
            itemCount: menuTitles.length + 1);
      }

    关于BoxDecoration的使用可以参考https://www.cnblogs.com/webor2006/p/12845831.html,其中用到了一个默认图像图:

    上面是个纯白的图哦,其实是长这样:

    记得要在yaml中声明哦:

    运行瞅一下效果:

    呃,样式不太对, 咱们先给它加一个背景颜色:

    @override
      Widget build(BuildContext context) {
        return ListView.separated(
            itemBuilder: (context, index) {
              if (index == 0) {
                //用户头像
                return Container(
                  color: Color(AppColors.APP_THEME),
                  height: 150.0,
                  child: Center(
                    child: Column(
                      children: <Widget>[
                        Container(
                           60.0,
                          height: 60.0,
                          decoration: BoxDecoration(
                              shape: BoxShape.circle,
                              border: Border.all(
                                color: Colors.white,
                                 2.0,
                              ),
                              image: DecorationImage(
                                image: AssetImage(
                                    'assets/images/ic_avatar_default.png'),
                                fit: BoxFit.cover,
                              )),
                        )
                      ],
                    ),
                  ),
                );
              }
              index -= 1;
              return ListTile(
                leading: Icon(menuIcons[index]), //左图标
                title: Text(menuTitles[index]), //中间标题
                trailing: Icon(Icons.arrow_forward_ios),
              );
            },
            separatorBuilder: (context, index) {
              return Divider();
            }, //分隔线
            itemCount: menuTitles.length + 1);
      }

    运行:

    头像木有居中,所以改一下:

    再运行:

    接下来则需要在它下面加一个用户名称的文本:

    @override
      Widget build(BuildContext context) {
        return ListView.separated(
            itemBuilder: (context, index) {
              if (index == 0) {
                //用户头像
                return Container(
                  color: Color(AppColors.APP_THEME),
                  height: 150.0,
                  child: Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        Container(
                           60.0,
                          height: 60.0,
                          decoration: BoxDecoration(
                              shape: BoxShape.circle,
                              border: Border.all(
                                color: Colors.white,
                                 2.0,
                              ),
                              image: DecorationImage(
                                image: AssetImage(
                                    'assets/images/ic_avatar_default.png'),
                                fit: BoxFit.cover,
                              )),
                        ),
                        SizedBox(
                          //加间距
                          height: 10.0,
                        ),
                        Text(
                          '点击头像登录',
                          style: TextStyle(color: Colors.white),
                        )
                      ],
                    ),
                  ),
                );
              }
              index -= 1;
              return ListTile(
                leading: Icon(menuIcons[index]), //左图标
                title: Text(menuTitles[index]), //中间标题
                trailing: Icon(Icons.arrow_forward_ios),
              );
            },
            separatorBuilder: (context, index) {
              return Divider();
            }, //分隔线
            itemCount: menuTitles.length + 1);
      }

    运行:

    貌似界面有点不太和谐呀,看这:

    所以将标题栏的阴影得去掉,加个属性配置就成了:

    再运行看一下:

    另外咱们给头像先增加一个点击事件,其它菜单列表暂且先放着不管,这次主要焦点是通过登录这个案例来学会Flutter的网络请求,这里则需要将头像用手势的widget包裹一下了:

    @override
      Widget build(BuildContext context) {
        return ListView.separated(
            itemBuilder: (context, index) {
              if (index == 0) {
                //用户头像
                return Container(
                  color: Color(AppColors.APP_THEME),
                  height: 150.0,
                  child: Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        GestureDetector(
                          child: Container(
                             60.0,
                            height: 60.0,
                            decoration: BoxDecoration(
                                shape: BoxShape.circle,
                                border: Border.all(
                                  color: Colors.white,
                                   2.0,
                                ),
                                image: DecorationImage(
                                  image: AssetImage(
                                      'assets/images/ic_avatar_default.png'),
                                  fit: BoxFit.cover,
                                )),
                          ),
                          onTap: () {
                            //TODO:执行登录
                          },
                        ),
                        SizedBox(
                          //加间距
                          height: 10.0,
                        ),
                        Text(
                          '点击头像登录',
                          style: TextStyle(color: Colors.white),
                        )
                      ],
                    ),
                  ),
                );
              }
              index -= 1;
              return ListTile(
                leading: Icon(menuIcons[index]), //左图标
                title: Text(menuTitles[index]), //中间标题
                trailing: Icon(Icons.arrow_forward_ios),
              );
            },
            separatorBuilder: (context, index) {
              return Divider();
            }, //分隔线
            itemCount: menuTitles.length + 1);
      }

    先写个TODO吧,由于跳转到登录时需要使用到了开源中国的API,所以,接下来将焦点转移一下,先来对开源中国的API有一个理性上的认识。

    开源中国API流程了解:

    先上官网https://www.oschina.net/openapi来了解一下,打开之后,先来读一下整体说明:

    嗯,等于是开放了开源中国移动APP的所有接口,其中第一台说到了采用了OAuth2认证,关于它的原理可以参考https://www.cnblogs.com/webor2006/p/10362197.html,然后往下读,则是比较重要的一个OAuth2的认证流程图,贴一下:

    创建应用:

    可以看到最终访问正式的API时是需要每个接口都携带AccessToken滴,而在它之前则需要经过若干步骤才能得到,流程图左边可以看到得先创建一个应用,所以下面先来创建一下:

    HTTP请求封装:

    接下来咱们则来对HTTP请求进行一个封装, 根据流程图步骤可以看到:

    在进行OAuth2授权时需要用到我们创建应用时的三个信息:

     

    那咱们先将这些数据配置到常量里面:

    然后在登录时需要用到两个接口:

    所以咱们也将这俩要用到的URL也封装到常量中:

    好,接下来则对HTTP进行一个封装,由于是个异步处理,所以会用到async..await,关于这块可以参考:https://www.cnblogs.com/webor2006/p/11994645.html, 对于Android我们平常会使用OkHttp来进行网络请求,而在Flutter也是借助三方库来进行,如下:

    这里其实有个小技巧,此时同步一下就可以使用它了,其实它的版本号在这里会自己获取:

    所以要想加版本号,则直接将它拷出来再声明上既可,如下:

    然后咱们再来使用一下它:

    import 'package:http/http.dart' as http;
    
    class NetUtils {
      static Future<String> get(String url, Map<String, dynamic> params) async {
        if (url != null && params != null && params.isNotEmpty) {
          //拼装参数
          StringBuffer sb = StringBuffer('?');
          params.forEach((key, value) {
            sb.write('$key=$value&');
          });
          //去掉最后一个&
          String paramStr = sb.toString().substring(0, sb.length - 1);
          url += paramStr;
        }
        print('NetUtils : $url');
        http.Response response = await http.get(url);
        return response.body;
      }
    
      static Future<String> post(String url, Map<String, dynamic> params) async {
        http.Response response = await http.post(url, body: params);
        return response.body;
      }
    }

    其中Map中用到了一个dynamic类型,还记得它么,可在参考:https://www.cnblogs.com/webor2006/p/11975291.html

    网络请求工具类封装好之后,接下来咱们就可以来实现登录逻辑了。

    登录处理:

    登录跳转到WebView:

    先来看一下官方的授权流程,其实可以看到需要用浏览器或WebView来请求:

    所以先得准备一个WebView页面,点击登录时先来跳转到这个页面,而在Flutter中当然也有WebView组件喽,这里先来集成一下:

     

    同样的技巧,先不用输入版本号,然后加载之后,在这可以查看到具体是用哪个版本号:

    接下来新建一个页面:

    import 'package:flutter/material.dart';
    import 'package:flutter_osc_client/constants/constants.dart'
        show AppColors, AppInfos, AppUrls;
    import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
    
    class LoginWebPage extends StatefulWidget {
      @override
      _LoginWebPageState createState() => _LoginWebPageState();
    }
    
    class _LoginWebPageState extends State<LoginWebPage> {
      FlutterWebviewPlugin _flutterWebviewPlugin = FlutterWebviewPlugin();
    
      @override
      void initState() {
        super.initState();
        //监听url变化
        _flutterWebviewPlugin.onUrlChanged.listen((url) {
          //https://www.oschina.net/action/oauth2/authorize?response_type=code&client_id=6i4Yu6IUqXnR64em0rsJ&redirect_uri=https://www.xxx.com/
          print('LoginWebPage onUrlChanged: $url');
    //      https://www.xxxx.com/?code=6hHYoH&state=
          if (url != null && url.length > 0 && url.contains('?code=')) {
            //登录成功了
          }
        });
      }
    
      @override
      void dispose() {
        super.dispose();
        _flutterWebviewPlugin.close();
      }
    
      @override
      Widget build(BuildContext context) {
        //authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https
        List<Widget> _appBarTitle = [
          Text(
            '登录开源中国',
            style: TextStyle(
              color: Color(AppColors.APPBAR),
            ),
          ),
        ];
        return WebviewScaffold(
          url: AppUrls.OAUTH2_AUTHORIZE +
              '?response_type=code&client_id=' +
              AppInfos.CLIENT_ID +
              '&redirect_uri=' +
              AppInfos.REDIRECT_URI,
          appBar: AppBar(
            title: Row(
              children: _appBarTitle,
            ),
          ),
          withJavascript: true,
          //允许执行js
          withLocalStorage: true,
          //允许本地存储
          withZoom: true, //允许网页缩放
        );
      }
    }

    具体代码也比较好理解,就不多解释了,标红的写法可以熟悉一下,也就是可以对里面的多个类进行show,接下来则链到点击头像的事件上面:

    关于页面路由的跳转细节可以参考https://www.cnblogs.com/webor2006/p/12545701.html,下面运行看一下效果:

    嗯,确实正常加载了,不过加载过程中木有loading框显示,所以接下来先来处理这个小细节:

    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_osc_client/constants/constants.dart'
        show AppColors, AppInfos, AppUrls;
    import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
    
    class LoginWebPage extends StatefulWidget {
      @override
      _LoginWebPageState createState() => _LoginWebPageState();
    }
    
    class _LoginWebPageState extends State<LoginWebPage> {
      FlutterWebviewPlugin _flutterWebviewPlugin = FlutterWebviewPlugin();
      bool isLoading = true;
    
      @override
      void initState() {
        super.initState();
        //监听url变化
        _flutterWebviewPlugin.onUrlChanged.listen((url) {
          //https://www.oschina.net/action/oauth2/authorize?response_type=code&client_id=6i4Yu6IUqXnR64em0rsJ&redirect_uri=https://www.xxx.com/
          print('LoginWebPage onUrlChanged: $url');
    //      https://www.xxxx.com/?code=6hHYoH&state=
          if (mounted) {
            setState(() {
              isLoading = false; //网页加载完取消loading框
            });
          }
          if (url != null && url.length > 0 && url.contains('?code=')) {
            //登录成功了
          }
        });
      }
    
      @override
      void dispose() {
        super.dispose();
        _flutterWebviewPlugin.close();
      }
    
      @override
      Widget build(BuildContext context) {
        //authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https
        List<Widget> _appBarTitle = [
          Text(
            '登录开源中国',
            style: TextStyle(
              color: Color(AppColors.APPBAR),
            ),
          ),
        ];
        if (isLoading) {
          //在标题栏上增加一个loading
          _appBarTitle.add(SizedBox(
             10.0,
          ));
          _appBarTitle.add(CupertinoActivityIndicator());
        }
        return WebviewScaffold(
          url: AppUrls.OAUTH2_AUTHORIZE +
              '?response_type=code&client_id=' +
              AppInfos.CLIENT_ID +
              '&redirect_uri=' +
              AppInfos.REDIRECT_URI,
          appBar: AppBar(
            title: Row(
              children: _appBarTitle,
            ),
          ),
          withJavascript: true,
          //允许执行js
          withLocalStorage: true,
          //允许本地存储
          withZoom: true, //允许网页缩放
        );
      }
    }

    其中有个代码可能比较懵逼:

    看一下它的说明就知道了:

    运行看一下效果:

    接下来咱们登录连接一下:

    看到我的博客主页了~~因为我们设置的回调地址就是我自己的个人博客:

    然后此时看一下我们打印的地址:

     

    很明显可以看到,在登录成功之后,给咱们的回调地址后面加上了一个code属性,还记得code是干嘛用的呢?回到授权流程图中:

    也就是接下来则需要解析这个code授权码再去请求一个新接口最终获取到关键的AccessToken,所以先来解析一下code值:

     

    获取AccessToken:

    接下来则需要拿这个code授权码请求这个接口来获取最终关键的AccessToken了:

    而它需要的参数为:

    所以接下来直接请求,看能否成功:

    运行看一下:

    2020-06-12 07:42:57.543 16751-16831/com.example.flutter_osc_client I/flutter: LoginWebPage onUrlChanged: https://www.cnblogs.com/webor2006/?code=naJpb7&state=
    2020-06-12 07:42:57.551 16751-16831/com.example.flutter_osc_client I/flutter: NetUtils : https://www.oschina.net/action/openapi/token?client_id=HfFS7FhSuffq5My6w3Lk&client_secret=710zTcDJ9NRxnLtsZvjFKgIHqaSD0JDg&grant_type=authorization_code&redirect_uri=https://www.cnblogs.com/webor2006/&code=naJpb7&dataType=json
    2020-06-12 07:42:58.413 16751-16831/com.example.flutter_osc_client I/flutter: oauth2_response:{"access_token":"d9736548-242d-42ce-82c4-e8421723017f","refresh_token":"f8ec95b2-4612-4633-bb96-fffc88a972d2","uid":1861912,"token_type":"bearer","expires_in":604799}

    保存用户信息:

    先对Json进行解析,那在Flutter当中Json解析需要像Android那样用三方的么?不需要了,因为已经内置到dart库中了,如下:

    import 'dart:convert';
    
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_osc_client/constants/constants.dart'
        show AppColors, AppInfos, AppUrls;
    import 'package:flutter_osc_client/utils/net_utils.dart';
    import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
    
    class LoginWebPage extends StatefulWidget {
      @override
      _LoginWebPageState createState() => _LoginWebPageState();
    }
    
    class _LoginWebPageState extends State<LoginWebPage> {
      FlutterWebviewPlugin _flutterWebviewPlugin = FlutterWebviewPlugin();
      bool isLoading = true;
    
      @override
      void initState() {
        super.initState();
        //监听url变化
        _flutterWebviewPlugin.onUrlChanged.listen((url) {
          //https://www.oschina.net/action/oauth2/authorize?response_type=code&client_id=6i4Yu6IUqXnR64em0rsJ&redirect_uri=https://www.xxx.com/
          print('LoginWebPage onUrlChanged: $url');
    //      https://www.xxxx.com/?code=6hHYoH&state=
          if (mounted) {
            setState(() {
              isLoading = false; //网页加载完取消loading框
            });
          }
          if (url != null && url.length > 0 && url.contains('?code=')) {
            //登录成功了,提取授权码code
            String code = url.split('?')[1].split('&')[0].split('=')[1];
            Map<String, dynamic> params = Map<String, dynamic>();
            params['client_id'] = AppInfos.CLIENT_ID;
            params['client_secret'] = AppInfos.CLIENT_SECRET;
            params['grant_type'] = 'authorization_code';
            params['redirect_uri'] = AppInfos.REDIRECT_URI;
            params['code'] = '$code';
            params['dataType'] = 'json';
            NetUtils.get(AppUrls.OAUTH2_TOKEN, params).then((data) {
              print('oauth2_response:$data');
              if (data != null) {
                Map<String, dynamic> map = json.decode(data);
                if (map != null && map.isNotEmpty) {
                  //TODO:保存token等信息
                }
              }
            });
          }
        });
      }

    而保存这里也是用SharedPreferences,那Android有它木有疑问,难道其它平台也有么?肯定是有类似的,Flutter是中间语言最终肯定会编译成相关平台的保存策略的,这里就不用过多操心了,这个就需要导一下库了,没有内置,如下:

    同样的技巧,先不写版本号,然后再查看一下版本号:

    然后咱们使用一下,先来封装一个数据保存的工具类:

    import 'package:shared_preferences/shared_preferences.dart';
    
    class DataUtils {
      static const String SP_ACCESS_TOKEN = 'access_token';
      static const String SP_REFRESH_TOKEN = 'refresh_token';
      static const String SP_UID = 'uid';
      static const String SP_TOKEN_TYPE = 'token_type';
      static const String SP_EXPIRES_IN = 'expires_in';
      static const String SP_IS_LOGIN = 'is_login';
    
      static Future<void> saveLoginInfo(Map<String, dynamic> map) async {
        if (map != null && map.isNotEmpty) {
          SharedPreferences sp = await SharedPreferences.getInstance();
          sp
            ..setString(SP_ACCESS_TOKEN, map[SP_ACCESS_TOKEN])
            ..setString(SP_REFRESH_TOKEN, map[SP_REFRESH_TOKEN])
            ..setInt(SP_UID, map[SP_UID])
            ..setString(SP_TOKEN_TYPE, map[SP_TOKEN_TYPE])
            ..setInt(SP_EXPIRES_IN, map[SP_EXPIRES_IN])
            ..setBool(SP_IS_LOGIN, true);
        }
      }
    
      static Future<void> clearLoginInfo() async {
        SharedPreferences sp = await SharedPreferences.getInstance();
        sp
          ..setString(SP_ACCESS_TOKEN, '')
          ..setString(SP_REFRESH_TOKEN, '')
          ..setInt(SP_UID, -1)
          ..setString(SP_TOKEN_TYPE, '')
          ..setInt(SP_EXPIRES_IN, -1)
          ..setBool(SP_IS_LOGIN, false);
      }
    
      //是否登录
      static Future<bool> isLogin() async {
        SharedPreferences sp = await SharedPreferences.getInstance();
        bool isLogin = sp.getBool(SP_IS_LOGIN);
        return isLogin != null && isLogin;
      }
    
      //获取token
      static Future<String> getAccessToken() async {
        SharedPreferences sp = await SharedPreferences.getInstance();
        return sp.getString(SP_ACCESS_TOKEN);
      }
    }

    代码比较简单,其使用跟Android的sp差不多,只是这里又用到了async,await异步处理代码,需要多熟悉:

    好,接下来调用一下它:

    通知刷新用户界面:

    在保存了token信息之后,则需要通知主界面根据access_token来获取用户的信息,也就是这个接口:

    所以先来处理通知我的界面进行刷新的逻辑,具体怎么通知呢?这里可以先使用路由返回一条消息:

    然后我们在路由跳转到登录界面的那个地方就可以来接收路由的返回信息了:

    但这种写法其实是有些问题的。。很显然目前这个方法是一个同步方法,而等待路由结果应该是一个异步等待,所以又得async await一下:

    好,接下来收到路由刷新通知之后,这里再用EventBus来通知请求用户信息接口,是的,Flutter中也有EventBus,当然先集成一下它喽:

    老套路,看一下版本号:

    接下来使用一下,看它跟在Android中有啥区别,先来定义一下事件:登入和登出:

    import 'package:event_bus/event_bus.dart';
    
    EventBus eventBus = EventBus();
    
    class LoginEvent {}
    
    class LogoutEvent {}

    然后使用一下:

    接下来先看一下能否收到登录成功的EventBus消息:

    嗯,确实是跳回到了用户界面,看一下日志输出:

    嗯,妥妥的,这次暂时先学到这,下次继续。 

  • 相关阅读:
    zypper命令使用示例
    《大数据之路:阿里巴巴大数据实践》——1-5章
    《织云 Metis 时间序列异常检测全方位解析
    《Replicator Neural Networks》
    《软件应用 | 用 R 语言做因果推断?你少不了这些包》
    《【统计】Causal Inference》
    《Google 开源AI项目15个》
    《TF-Replicator:研究人员的分布式机器学习》
    《TF-Replicator, GPipe, Mesh-Tensorflow 三个库对比》
    虚拟对抗训练:一种新颖的半监督学习正则化方法
  • 原文地址:https://www.cnblogs.com/webor2006/p/13028104.html
Copyright © 2011-2022 走看看