Skip to content

Yiend/flutter-cnblog

Folders and files

NameName
Last commit message
Last commit date

Latest commit

author
叶秋
May 14, 2019
ff7ca01 · May 14, 2019

History

14 Commits
Apr 29, 2019
Apr 29, 2019
May 13, 2019
May 14, 2019
May 13, 2019
May 13, 2019
May 13, 2019
Apr 29, 2019
May 13, 2019
May 4, 2019
Apr 29, 2019
May 13, 2019
May 14, 2019
Apr 29, 2019
May 13, 2019
May 13, 2019

Repository files navigation

Flutter版 博客园第三方 App.

如何疑问或BUG 请提 Issue

注意

访问博客园API需要申请API KEY,申请地址

为了下载项目后能直接运行,已经设置了默认的Key,请申请成功后替换自己的Key.

App目录结构

  • |--android
  • |--assets (静态资源文件)
  • |--build (编译文件)
  • |--ios
  • |--lib
    • |-- blocs
    • |-- common (公共类,帮助类,例如常量Constant)
    • |-- components (组件)
    • |-- model (实体)
    • |-- pages (界面相关)
    • |-- resources (资源,strings,colors)
    • |-- services (服务层)
    • |-- utils (工具)
    • |-- widgets (部件,webview,loading,refresh,item ...)
  • |--test

网络数据

本项目使用 Dio 进行网络请求。具体实现在 utils --> http_util

class HttpUtil {
  Dio _dio;
  Dio _tokenDio;
  BaseOptions options;
  static final _baseUrl = AppConfig.host;
  static final _jsonContentType = ContentType.parse("application/json");
  static final _formContentType =
      ContentType.parse("application/x-www-form-urlencoded");

  // 工厂模式
  factory HttpUtil() => _getInstance();
  static HttpUtil get instance => _getInstance();
  static HttpUtil _instance;

  static HttpUtil _getInstance() {
    if (_instance == null) {
      _instance = new HttpUtil._internal();
    }
    return _instance;
  }

  HttpUtil._internal() {
    // 初始化
    options = BaseOptions(
        baseUrl: _baseUrl,
        connectTimeout: 10000,
        receiveTimeout: 5000,
        headers: {},
        followRedirects: true);
    _dio = new Dio(options);
    _tokenDio = new Dio(options);

    //拦截器
    _dio.interceptors
        .add(InterceptorsWrapper(onRequest: (RequestOptions options) {
      // 在请求被发送之前做一些事情
      if (options.path.startsWith("token")) {
        return options;
      }

      var sptokenModel = SpHelper.getObject<TokenModel>(CacheKey.access_token);
      if (sptokenModel == null ||sptokenModel.access_token==null) {
        _dio.interceptors.requestLock.lock();

        Map<String, String> map = new Map();
        map['grant_type'] = 'client_credentials';
        map['client_id'] = AppConfig.clientId;
        map['client_secret'] = AppConfig.clientSecret;

        var option = new Options(contentType: _formContentType);
        return _tokenDio
            .post(AppConfig.clientToken, data: map, options: option)
            .then((result) {
          var tokenModel = TokenModel.fromJson(result.data);
          if (tokenModel != null) {
            SpHelper.setObject(CacheKey.access_token, tokenModel.toJson());
          }

          options.headers["Authorization"] = 'Bearer ${tokenModel.access_token}';
          return options;
        }).whenComplete(
                () => _dio.interceptors.requestLock.unlock()); // unlock the dio

      } else {
        options.headers["Authorization"] = 'Bearer ${sptokenModel.access_token}';
        return options; //continue
      }

      // 如果你想完成请求并返回一些自定义数据,可以返回一个`Response`对象或返回`dio.resolve(data)`。
      // 这样请求将会被终止,上层then会被调用,then中返回的数据将是你的自定义数据data.
      //
      // 如果你想终止请求并触发一个错误,你可以返回一个`DioError`对象,或返回`dio.reject(errMsg)`,
      // 这样请求将被中止并触发异常,上层catchError会被调用。
    }, onResponse: (Response response) {
      // 在返回响应数据之前做一些预处理
      return response; // continue
    }, onError: (DioError e) {
      // 当请求失败时做一些预处理
      return e; //continue
    }));
    
    .......
  }

说明

文章的详情展示,由于API 返回的不是完整的页面Url,只是文章内容,无法直接用WebView 展示,故自己写了静态html,将内容通过Vue的方式 绑定到页面展示。

又由于静态的html 无法加载样式与JavaScript ,我在其App 内部开启一个web服务器。核心代码如下:

void main() async {
  //开启本地web服务。用于加载样式
  final server = Jaguar();
  server.addRoute(serveFlutterAssets());
  await server.serve(logRequests: true);
  server.log.onRecord.listen((r) => print(r));
  // end web server
  
  
  runApp(BlocProvider<ApplicationBloc>(
    bloc: ApplicationBloc(),
    child: MyApp()//BlocProvider(child: MyApp(), bloc: HomeBloc()),
  ));
}

html 部分

<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0,minimum-scale=1.0, user-scalable=no" />
    <link rel="stylesheet" type="text/css" href="http://127.0.0.1:8080/css/default.css" />
    <script type="text/javascript" src="http://127.0.0.1:8080/js/jquery.min.js"></script>
    <script type="text/javascript" src="http://127.0.0.1:8080/js/vue.min.js"></script>
</head>
<body>
    <div id="app" v-cloak v-show="hasContent">
        <h2 class="title">{{ model.title }}</h2>
        <div class="authorinfo">
            <div class="authorInfo-content">
                <span class="authorInfo-time">
                    发布于 · {{ model.dateDisplay }} · {{ model.diggCount }} 推荐 · {{ model.viewCount }} 阅读
                </span>
            </div>
        </div>
        <div class="content" v-html="model.body"></div>
        <div class="comments">
            <div class="comment-header">
                <div class="line"></div>
                <div class="comment">所有评论</div>
                <div class="line">
                </div>
            </div>
            <section v-for="item in comments">
                <div class="comments-item">
                    <span class="item-avatar"><img class="avatar" width="45" height="45" v-bind:src="item.FaceUrl==''?'http://127.0.0.1:8080/images/avatar_placeholder.png':item.faceUrl" /></span>
                    <div class="item-authorInfo">
                        <span class="item-name">{{ item.userName }}</span>
                        <div class="item-content" v-html="item.commentContent"></div>
                        <div class="item-actions">
                            <span class="item-time">{{ item.dateDisplay }}</span>
                            <div class="item-edit" v-if="item.IsLoginUser">
                                <img src="http://127.0.0.1:8080/images/ic_edit.png" v-on:click="editItem(item)" />
                                <img src="http://127.0.0.1:8080/images/loading.gif" v-if="item.IsDelete" />
                                <img src="http://127.0.0.1:8080/images/ic_delete.png" v-on:click="deleteItem(item)" v-else />
                            </div>
                        </div>
                        <span class="item-line"></span>
                    </div>
                </div>
            </section>
        </div>
        <div class="footer">
            <div class="loading" v-if="loadStatus == 0||loadStatus == 1">
                <img src="http://127.0.0.1:8080/images/loading.gif" class="loading-img" /><span class="loading-content">正在加载中</span>
            </div>
            <div class="nodata" v-else-if="loadStatus == 2">
                - 还没有评论 -
            </div>
            <div class="nodata" v-else-if="loadStatus == 3">
                - 没有更多评论了 -
            </div>
            <div class="error" v-else-if="loadStatus === 4||loadStatus === 5">
                <span class="error-content">好像出现了问题</span>
                <a href="javascript:void(0)" class="error-btn" v-on:click="reload">重新加载</a>
            </div>
        </div>
    </div>
    <script>
        var app = new Vue({
            el: '#app',
            data: {
                hasContent: false,
                model: {},
                loadStatus: 0,
                comments: []
            },
            methods: {
                reload: function () {
                    this.loadStatus = 1;
                    flutterReload.postMessage("123");
                },
                editItem: function (item) {
                    editItem(item.CommentID);
                },
                deleteItem: function (item) {
                    item.IsDelete = true;
                    deleteItem(item.CommentID);
                }
            }
        });
        $(window).scroll(function () {
            loadReload();
        });
        function scrollToComments() {
            if (app.hasContent) {
                $("html,body").animate({ scrollTop: $(".comments").offset().top }, 1000);
            }
        }
        function loadReload() {
            if ($(document).scrollTop() + window.screen.height + 70 >= $(document).height()) {
                if (app.loadStatus == 0 || app.loadStatus == 2){
                     flutterReload.postMessage("123");
                }
            }
        }
        function updateModel(model) {
            if (!app.hasContent) {
                app.hasContent = true;
                loadReload();
            }
            app.model = model;
        }
        function updateLoadStatus(loadStatus) {
            app.loadStatus = loadStatus;
        }
        function updateComments(list) {
            app.comments = app.comments.concat(list)
        }
        function updateComment(item) {
            var b = true;
            if (item.CommentID > 0) {
                for (var i = 0; i < app.comments.length; i++) {
                    var element = app.comments[i];
                    if (element.CommentID == item.CommentID) {
                        b = false;
                        element = item;
                    }
                }
            }
            if (b) {
                app.comments = app.comments.concat(item);
            }
        }
        function deleteComment(id, isTrue) {
            for (var i = 0; i < app.comments.length; i++) {
                var element = app.comments[i];
                if (element.CommentID == id) {
                    element.IsDelete = isTrue;
                    if (isTrue) {
                        app.comments = app.comments.splice(i, 1);
                    }
                }
            }
        }
    </script>
</body>
</html>

Flutter 国际化相关 fluintl

fluintl是一个为应用提供国际化的库,可快速集成实现应用多语言。该库封装了一个国际化支持类,通过提供统一方法getString(id)获取字符串。

// 在MyApp initState配置多语言资源
setLocalizedValues(localizedValues); //配置多语言资源
// 在MaterialApp指定localizationsDelegates和supportedLocales
MaterialApp(  
   home: MyHomePage(),  
   localizationsDelegates: [  
   GlobalMaterialLocalizations.delegate,  
   GlobalWidgetsLocalizations.delegate,  
   CustomLocalizations.delegate //设置本地化代理     
   ],  
   supportedLocales: CustomLocalizations.supportedLocales,//设置支持本地化语言集合     
); 
// 字符串获取
IntlUtil.getString(context, Ids.titleHome);

关于Android版

截图

assets assets assets assets assets

关于IOS版

太穷,买不起Mac, 没有环境,跑不起来 emmmmmm......嘤嘤嘤~~~

联系我

如果你不再恐惧,你会做什么?