├── .gitignore ├── LICENSE ├── README.md ├── android ├── build.gradle ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── io │ └── flutter │ └── plugins │ └── wuba │ └── magpielog │ ├── MagpieLogListener.java │ └── MagpieLogPlugin.java ├── doc ├── magpie_log_share.md └── media │ ├── 15782168162404.jpg │ ├── 15782185559383.jpg │ ├── framework.png │ ├── hairy_tree.png │ ├── magpie_log_video.gif │ ├── medium_tree.png │ ├── redux-architecture-overview-middleware.png │ └── 配置文件.png ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ ├── com │ │ │ │ │ └── wuba │ │ │ │ │ │ └── flutter │ │ │ │ │ │ └── example │ │ │ │ │ │ ├── App.java │ │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ │ ├── NormalActivity.java │ │ │ │ │ │ └── PageRouter.java │ │ │ │ └── io │ │ │ │ │ └── flutter │ │ │ │ │ └── plugins │ │ │ │ │ └── GeneratedPluginRegistrant.java │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── layout │ │ │ │ └── activity_main.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── icon.jpg │ │ │ │ └── logo.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ └── values │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── local.properties │ └── settings.gradle ├── assets │ └── analysis.json ├── images │ ├── list.png │ ├── list_selected.png │ ├── page.png │ ├── page_selected.png │ ├── redux.png │ ├── redux_selected.png │ ├── state.png │ ├── state_selected.png │ ├── verbose.png │ └── verbose_selected.png ├── ios │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ ├── Generated.xcconfig │ │ └── Release.xcconfig │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ └── contents.xcworkspacedata │ └── Runner │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── GeneratedPluginRegistrant.h │ │ ├── GeneratedPluginRegistrant.m │ │ ├── Info.plist │ │ └── main.m ├── lib │ ├── flutterboost │ │ ├── main_flutter_boost.dart │ │ └── page_router.dart │ ├── main.dart │ ├── states │ │ ├── app_state.dart │ │ └── app_state.g.dart │ ├── top_screen.dart │ ├── under_screen.dart │ └── utils.dart └── pubspec.yaml ├── gradlew ├── gradlew.bat ├── images ├── check_icon.png └── uncheck_icon.png ├── ios ├── Classes │ ├── MagpieLogPlugin.h │ └── MagpieLogPlugin.m └── magpie_log.podspec ├── lib ├── file │ ├── data_analysis.dart │ ├── data_statistics.dart │ ├── file_utils.dart │ └── log_util.dart ├── handler │ └── statistics_handler.dart ├── interceptor │ ├── intercepter_screen_log.dart │ ├── interceptor_circle_log.dart │ └── interceptor_state_log.dart ├── magpie_constants.dart ├── magpie_log.dart ├── model │ ├── analysis_model.dart │ ├── analysis_model.g.dart │ ├── device_data.dart │ └── device_data.g.dart └── ui │ ├── log_actiion_list.dart │ ├── log_float_view.dart │ ├── log_operation_screen.dart │ ├── log_screen.dart │ └── log_select_report.dart ├── local.properties └── pubspec.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Exceptions to above rules. 37 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 38 | pubspec.lock 39 | .build_ios/ 40 | stash 41 | 42 | example/android/.idea 43 | example/build/ 44 | android/build/ 45 | example/android/.gradle -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2005-present, 58.com. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | * Neither the name of Google Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A visualized dynamic programming for log collection based on flutter. 2 | 3 | # Pub使用 4 | ### 1. Depend on it 5 | Add this to your package's pubspec.yaml 6 | ``` 7 | dependencies: 8 | magpie_log: ^1.0.1 9 | ``` 10 | 11 | ### 2. Install it 12 | You can install packages from the command line: 13 | 14 | ``` 15 | $ flutter pub get 16 | ``` 17 | 18 | Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more. 19 | 20 | ### 3. Import it 21 | Now in your Dart code, you can use: 22 | 23 | ``` 24 | import 'package:magpie_log/magpie_log.dart'; 25 | ``` 26 | # Dart使用方式 27 | 在自己工程首页调用`MagpieLog.instance.init`初始化方法 28 | ``` 29 | Widget build(BuildContext context) { 30 | MagpieLog.instance.init(context, 31 | ReportMethod.timing, 32 | ReportChannel.natives, 33 | callback: _receiverMagpieData, 34 | time: 1 * 60 * 1000, 35 | count: 3); 36 | return Container(); 37 | } 38 | ``` 39 | 这里需要注意context必须是包含Navigator的context 40 | ### redux圈选使用方式 41 | redux圈选 你的工程需要使用flutter-redux做状态管理 42 | 需要注入一个中间件`CircleMiddleWare` 43 | ``` 44 | final store = Store(reducer, 45 | middleware: [CircleMiddleWare()], initialState: AppState.initState()); 46 | ``` 47 | 48 | 49 | 其中的AppState也就是store存储的数据集必须继承`LogState` 50 | 因为我们需要toJson方法才能更好的获取参数 51 | ``` 52 | class AppState extends LogState { 53 | Map toJson() => _$AppStateToJson(this); 54 | } 55 | ``` 56 | 随后就可以使用我们的redux圈选 57 | ### 页面圈选使用 58 | 需要注册navigator监听`LogObserver`,当然我们需要你显示的设置页面settings这样我们才能获取页面的唯一标识 59 | ``` 60 | MaterialApp( 61 | routes: { 62 | '/': (BuildContext context) => TopScreen(), 63 | '/UnderScreen': (BuildContext context) => UnderScreen(), 64 | }, 65 | navigatorObservers: [ 66 | LogObserver(), 67 | ], 68 | title: 'Flutter Demo', 69 | theme: ThemeData(primarySwatch: Colors.deepOrange), 70 | ), 71 | ``` 72 | ### setState()圈选使用 73 | 74 | 继承`WidgetLogState`即可实现对应方法 75 | ``` 76 | class AddTextState extends WidgetLogState { 77 | @override 78 | Map toJson() { 79 | Map map = Map(); 80 | map["count"] = count.toString(); 81 | return map; 82 | } 83 | 84 | @override 85 | String getActionName() { 86 | return "AddText"; 87 | } 88 | 89 | @override 90 | int getIndex() { 91 | return 0; 92 | } 93 | } 94 | ``` 95 | 96 | # 待完善 97 | 1.服务端配合,动态化配置的上传和加载 98 | 99 | 2.级联赋值问题 导致日志参数和数据一样,可以增加用户配置 100 | 101 | 3.store 参数为空 级联圈参null不全问题 102 | 103 | # 致谢 104 | 105 | 1.感谢PonnamKarthik的[fluttertoast](https://github.com/PonnamKarthik/FlutterToast)插件 106 | 107 | 2.感谢Rubber的[rubber](https://github.com/rubber/rubber)插件 108 | 109 | 110 | # Mapie系列链接 111 | 112 | Mapie包含了一系列的开源项目,访问对应仓库以便了解更多。 113 | 114 | > Magpie Workflow 115 | 116 | Flutter可视化工作流。 [https://github.com/wuba/magpie](https://github.com/wuba/magpie) 117 | 118 | > Magpie Native&Dart SDK 119 | 120 | 与Workflow配套,用于接入App,Flutter的SDK。[https://github.com/wuba/magpie_sdk](https://github.com/wuba/magpie_sdk) 121 | 122 | > Magpie Fly 123 | 124 | 所见即所得的Flutter UI组件库。[https://github.com/wuba/magpie_fly](https://github.com/wuba/magpie_fly) 125 | 126 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'io.flutter.pugins.wuba.magpielog' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | repositories { 6 | maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } 7 | maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' } 8 | maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' } 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:3.2.1' 13 | } 14 | } 15 | 16 | rootProject.allprojects { 17 | repositories { 18 | maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } 19 | maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' } 20 | maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' } 21 | } 22 | } 23 | 24 | apply plugin: 'com.android.library' 25 | 26 | android { 27 | compileSdkVersion 29 28 | 29 | defaultConfig { 30 | minSdkVersion 16 31 | targetSdkVersion 29 32 | signingConfig signingConfigs.debug 33 | versionCode = 1 34 | versionName = '1.0.0' 35 | } 36 | lintOptions { 37 | disable 'InvalidPackage' 38 | } 39 | compileOptions { 40 | sourceCompatibility = 1.8 41 | targetCompatibility = 1.8 42 | } 43 | buildToolsVersion = '28.0.3' 44 | } 45 | 46 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'magpie_log' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/java/io/flutter/plugins/wuba/magpielog/MagpieLogListener.java: -------------------------------------------------------------------------------- 1 | package io.flutter.plugins.wuba.magpielog; 2 | 3 | /** 4 | * Flutter数据监听回调 5 | */ 6 | public interface MagpieLogListener { 7 | 8 | void magpieDataListener(String jsonData); 9 | } 10 | -------------------------------------------------------------------------------- /android/src/main/java/io/flutter/plugins/wuba/magpielog/MagpieLogPlugin.java: -------------------------------------------------------------------------------- 1 | package io.flutter.plugins.wuba.magpielog; 2 | 3 | import io.flutter.plugin.common.BasicMessageChannel; 4 | import io.flutter.plugin.common.BinaryMessenger; 5 | import io.flutter.plugin.common.PluginRegistry; 6 | import io.flutter.plugin.common.StringCodec; 7 | 8 | public class MagpieLogPlugin implements BasicMessageChannel.MessageHandler { 9 | 10 | private final String MSG_CHANNEL_TAG = "magpie_analysis_channel"; 11 | 12 | private MagpieLogListener mLogListener; 13 | 14 | public static void registerWith(PluginRegistry.Registrar registrar) { 15 | getInstance().registerMsgChannel(registrar.messenger()); 16 | } 17 | 18 | public static MagpieLogPlugin getInstance(){ 19 | return LogPluginHolder.INSTANCE; 20 | } 21 | 22 | private void registerMsgChannel(BinaryMessenger messenger){ 23 | final BasicMessageChannel msgChannel = new BasicMessageChannel<>(messenger, MSG_CHANNEL_TAG, StringCodec.INSTANCE); 24 | msgChannel.setMessageHandler(this::onMessage); 25 | } 26 | 27 | public void registerLogListener(MagpieLogListener logListener){ 28 | this.mLogListener = logListener; 29 | } 30 | 31 | @Override 32 | public void onMessage(String jsonString, BasicMessageChannel.Reply reply) { 33 | if (mLogListener != null){ 34 | mLogListener.magpieDataListener(jsonString); 35 | } 36 | } 37 | 38 | private static class LogPluginHolder{ 39 | private static final MagpieLogPlugin INSTANCE = new MagpieLogPlugin(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /doc/magpie_log_share.md: -------------------------------------------------------------------------------- 1 | [toc] 2 | 3 | 作者:58 Magpie技术团队 4 | 5 | ------- 6 | 7 | # 背景 8 | 由于flutter还是一个比较新的技术,flutter的埋点方案还是停留在手动埋点,这样开发效率低,耦合严重;因此我们为flutter提供一套圈选埋点的解决方案,使埋点配置,选参可视化,便捷化,动态化,而且基于redux的特点我们可以解决传统圈选埋点无法选参的问题。 9 | # 架构 10 | ![架构](media/framework.png) 11 | 架构主要分为4个部分,圈选,配置,埋点,日志。 12 | 13 | 圈选埋点主要做了三种事件的拦截,页面级别事件基于Redux拦截action事件;页面跳转事件基于Navigator拦截页面跳转事件;局部点击刷新事件通过拦截setState方法实现。 14 | 15 | 配置部分主要为圈选埋点服务,包括配置的UI部分,圈选页面圈选配置页面等。配置管理部分主要是对配置文件的管理包括写入,读取拷贝,文件化存储等,将来可以通过服务网络下发配置,进一步简化流程同时可以实现埋点的动态化配置。 16 | 17 | 手动埋点因为圈选埋点不可能覆盖100%的埋点,因此我们提供了手动埋点的能力,最终在运行时和圈选埋点走同一套上传逻辑,这样做统一接口,统一逻辑,使接入更加的方便,全面。 18 | 19 | 日志部分指的是release模式下,上报埋点日志,我们通过api统一收集圈选和手动埋点的数据,为保证flutter和native频繁交互影响性能,我们提供给了定时器和计数器,并且提供了flutter上报和native上报两种方式供用户选择。 20 | 21 | 22 | 23 | # 圈选 24 | 25 | 这块是我们花了大量时间探索的地方,一度怀疑人生。设计之初我们一直沉浸于向native一样想找到拦截所有点击事件的方式,可以实现,但是十分复杂,而且非常不好维护。没有onclick。我们找到GestureDetector拦截ontap,由于Widget本身没有树,无法获取树形结构,再加上没有viewid,我们无法获取唯一标识,我们想用GlobalKey替代;一切好像可以实现,但是一切却又在逆天而行。其实最大的问题是我们一直在用native的思路去想flutter,及时实现,圈选埋点的条件也过于苛刻,整个工程引用起来也会过于沉重。最后我们统一思想,全部推翻重来,撇弃native的逻辑驱动,撇弃aop,迎合flutter的数据驱动,状态管理;从拦截点击事件,到拦截redux状态管理的action事件,局部刷新事件,我们通过拦截setState()方法。 26 | 27 | ## redux拦截实现 28 | 如果你的工程不是基于flutter,那么很遗憾,你的项目一定是下面两种之一:过于简单,或者状态管理混乱。 29 | 30 | ![medium_tree-w343](media/medium_tree.png)![hairy_tree-w469](media/hairy_tree.png) 31 | 32 | 你的项目不太适用我们的圈选,或者你可以看下面的setState部分, 33 | 如果你使用了redux,那么恭喜你,你的app非常的优秀,依托状态管理,只需要简单的设置,即可使用我们的圈选功能。 34 | 35 | 下图是flutter-redux的原理图 36 | ![redux-architecture-overview-middleware](media/redux-architecture-overview-middleware.png) 37 | 我们的圈选主要拦截的是Middleware这个部分,因为这个部分对原有逻辑的影响最小。 38 | 39 | ``` 40 | class CircleMiddleWare extends MiddlewareClass { 41 | @override 42 | void call(Store store, action, NextDispatcher next) { 43 | LogState logState = store.state; 44 | Map json = logState.toJson(); 45 | String actionName = 46 | action is LogAction ? action.actionName : action.toString(); 47 | String pagePath = MagpieLog.instance.getCurrentPath(); 48 | if (MagpieLog.instance.isDebug) { 49 | ///进入圈选页面 50 | } else { 51 | ///埋点并且走原有逻辑 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | ## list类型拦截 58 | 这里单拉出来讲一下列表点击事件的拦截,主要原因是列表数据结构较特殊,除了拦截事件还要拦截事件触发在列表的位置,在圈选参数和和配置上传上,都需要特殊处理。 59 | ``` 60 | store.dispatch(LogAction(actionAddCount)); 61 | ``` 62 | ``` 63 | store.dispatch(LogAction(actionListClick, index: index)); 64 | ``` 65 | 可以看到列表页埋点需要多配置index属性。 66 | 参数和配置上传在后面详细说明。 67 | 68 | ## 页面跳转拦截实现 69 | 页面跳转的拦截主要通过NavigatorObserver 70 | 71 | ``` 72 | class LogObserver extends NavigatorObserver { 73 | @override 74 | void didPush(Route route, Route previousRoute) async { 75 | // 当调用Navigator.push时回调 76 | MagpieLog.instance.routeStack.add(route); 77 | super.didPush(route, previousRoute); 78 | if (!route.settings.name.startsWith("/magpie_log")) { 79 | await Future.delayed(Duration(milliseconds: 500)); 80 | try { 81 | LogState logState = 82 | StoreProvider.of(route.navigator.context).state as LogState; 83 | var json = logState.toJson(); 84 | String actionName = 85 | route.settings.name != null ? route.settings.name : ""; 86 | String pagePath = MagpieLog.instance.getCurrentPath(); 87 | if (MagpieLog.instance.isDebug && MagpieLog.instance.isPageLogOn) { 88 | ///进入圈选页面 89 | } else { 90 | ///埋点并且走原有逻辑 91 | } 92 | } catch (e) { 93 | debugPrint(e.toString()); 94 | } 95 | } 96 | } 97 | } 98 | ``` 99 | 拦截的逻辑和redux的类似,对所有的push操作进行拦截过滤,首先要排除我们自己的ui页面:比如圈选和配置页面 100 | 101 | 为了随时获取当前显示的页面 我们自己维护了一个路由堆栈 102 | `MagpieLog.instance.routeStack` 103 | 104 | 但是为了页面有唯一的标识,以及点击等事件有页面路径作为标识需要确保配置好页面setting。页面的setting可以在路由初始化和跳转等地方设置。 105 | 106 | ## setState拦截展示 107 | setState这边做的是比较简单的实现 需要使用我们的WidgetLogState替代原有的State即可 108 | WidgetLogState源码如下所示 109 | ``` 110 | abstract class WidgetLogState extends State { 111 | String getActionName(); 112 | Widget onBuild(BuildContext context); 113 | @override 114 | void setState(VoidCallback fn) { 115 | Map json = toJson(); 116 | print("MyMiddleWare call:${json.toString()}"); 117 | var actionName = getActionName(); 118 | String pagePath = MagpieLog.instance.getCurrentPath(); 119 | if (MagpieLog.instance.isDebug) { 120 | ///进入圈选页面 121 | } else { 122 | ///埋点并且走原有逻辑 123 | } 124 | } 125 | Map toJson(); 126 | } 127 | ``` 128 | 129 | # 配置 130 | ## 圈选UI 131 | ### 圈选页面展示 132 | ![](media/15782168162404.jpg) 133 | 134 | 抽屉栏的设计方便看到自己在那个页面做了圈选操作。 135 | 基础配置部分: 136 | 埋点和修改两种展示方式方便区分已有配置和新增配置的区别; 137 | 事件标识 页面路径 埋点类型(可选)作为埋点的唯一标识; 138 | 描述信息可以自定义添加,作为备注; 139 | 参数配置部分 140 | 主要是一个递归的参数获取逻辑和选择逻辑 141 | 这里的list部分做了特殊处理,取的是list里面item的参数 142 | 这块代码如下: 143 | 144 | ``` 145 | ///递归参数数据初始化 146 | bool initParam(Map data, Map logConfig, List paramList) { 147 | if (data == null) return false; 148 | 149 | bool isPaneled = false; //是不是展开 150 | bool isParentPaneled = false; //父View是不是展开 151 | data.forEach((k, v) { 152 | List paramList2 = []; 153 | bool isChecked = false; 154 | if (v is Map) { 155 | isPaneled = 156 | initParam(v, logConfig == null ? null : logConfig[k], paramList2); 157 | } else if (v is List && v != null && v.length > 0) { 158 | isPaneled = initParam( 159 | v[0], 160 | logConfig == null || logConfig[k] == null ? null : logConfig[k], 161 | paramList2); 162 | } else { 163 | if (logConfig != null && logConfig[k] != null && logConfig[k] == 1) { 164 | isChecked = true; 165 | isParentPaneled = true; 166 | } 167 | } 168 | paramList.add(ParamItem(k, v.toString(), 169 | paramItems: paramList2, isChecked: isChecked, isPaneled: isPaneled)); 170 | }); 171 | //自己施展开的或者父View是展开的都要返回true 172 | return isPaneled || isParentPaneled; 173 | } 174 | ``` 175 | 176 | 这里需要注意当点击埋点操作时,实际是生成配置文件到内存中,只有点击保存操作才会存入文件 177 | 跳过操作就是走正常的原有逻辑。 178 | 179 | ### 圈选配置页面 180 | ![](media/15782185559383.jpg) 181 | 182 | 从左到右 埋点配置页面,配置管理页面,数据上报方式选择页面 183 | 184 | ## 配置文件部分 185 | ![配置文件](media/%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6.png) 186 | 187 | 配置文件是用来存储圈选埋点配置 188 | 189 | 为避免频繁的读取文件我们加了一级缓存我们的所有api操作都是与缓存交互,只有点击保存按钮,才会同步到data/data文件中。(以android为例) 190 | 191 | 在所有埋点圈选完成后,需要手动去data/data里面取到文件放入工程assets里面,后期有服务端支持可以直接上传到服务端,避免这步的操作 192 | 193 | assets只有在初始化时复制到data/data里面 194 | 195 | # 手动埋点 196 | 如下所示,不用过多赘述 197 | 198 | ``` 199 | MagpieStatisticsHandler.instance.writeData({'data': '手动埋点数据示例'}); 200 | ``` 201 | # 运行时日志 202 | ## 定时器,计数器 性能影响图 203 | 因为flutter和native频繁通讯必然对性能有影响,所以我们在交互之前添加了定时器和计数器,并交由用户自定义设置,方便接入使用,同时为避免缓存期间的日志丢失我们做了文件备份的功能避免埋点数据丢失 204 | ## 上报 205 | 由于大部分flutter app没有自己的埋点上报策略。所以我们在提供flutter回调接口的同时, 206 | 提供了native上报的能力,因为大部分app都有自己的native侧成熟的埋点上报体系。\ 207 | flutter上报:只需要在初始化方法里面实现对应回调即可。\ 208 | native上报:我们提供了channel和native的sdk,只需要native侧实现对应的具体实现即可。 209 | # 优化空间 210 | 1.服务端配合,动态化配置的上传和加载 211 | 2.级联赋值问题 导致日志参数和数据一样,可以增加用户配置 212 | 3.store 参数为空 级联圈参null不全问题 213 | 214 | # 视频演示![magpie_log_video](media/magpie_log_video.gif) 215 | 216 | -------------------------------------------------------------------------------- /doc/media/15782168162404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/doc/media/15782168162404.jpg -------------------------------------------------------------------------------- /doc/media/15782185559383.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/doc/media/15782185559383.jpg -------------------------------------------------------------------------------- /doc/media/framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/doc/media/framework.png -------------------------------------------------------------------------------- /doc/media/hairy_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/doc/media/hairy_tree.png -------------------------------------------------------------------------------- /doc/media/magpie_log_video.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/doc/media/magpie_log_video.gif -------------------------------------------------------------------------------- /doc/media/medium_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/doc/media/medium_tree.png -------------------------------------------------------------------------------- /doc/media/redux-architecture-overview-middleware.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/doc/media/redux-architecture-overview-middleware.png -------------------------------------------------------------------------------- /doc/media/配置文件.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/doc/media/配置文件.png -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | pubspec.lock 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | **/ios/**/Runner/GeneratedPluginRegistrant.h 43 | **/ios/**/GeneratedPluginRegistrant.m 44 | **/ios/**/.symlinks/ 45 | **/ios/**/Flutter/ 46 | **/ios/**/Pods/ 47 | **/ios/**/Runner.* 48 | **/ios/**/*.xcconfig 49 | **/ios/**/*.json 50 | **/ios/**/.gitkeep 51 | **/ios/**/magpie_log_plugin.podspec 52 | 53 | 54 | # Exceptions to above rules. 55 | !**/ios/**/default.mode1v3 56 | !**/ios/**/default.mode2v3 57 | !**/ios/**/default.pbxuser 58 | !**/ios/**/default.perspectivev3 59 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 60 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 20e59316b8b8474554b38493b8ca888794b0234a 8 | channel: v1.7.8-hotfixes 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter example 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 28 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "com.wuba.flutter.example" 37 | minSdkVersion 19 38 | targetSdkVersion 28 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 42 | 43 | } 44 | 45 | buildTypes { 46 | release { 47 | // TODO: Add your own signing config for the release build. 48 | // Signing with the debug keys for now, so `flutter run --release` works. 49 | signingConfig signingConfigs.debug 50 | } 51 | debug { 52 | minifyEnabled false 53 | useProguard false 54 | } 55 | } 56 | packagingOptions { 57 | exclude 'META-INF/proguard/androidx-annotations.pro' 58 | } 59 | 60 | // splits { 61 | // abi { 62 | // enable true 63 | // reset() 64 | // universalApk false 65 | // include 'arm64-v8a'//'armeabi'//'armeabi-v7a', 'arm64-v8a', 'x86_64' 66 | // } 67 | // } 68 | 69 | } 70 | 71 | flutter { 72 | source '../..' 73 | } 74 | 75 | dependencies { 76 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 77 | testImplementation 'junit:junit:4.12' 78 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 79 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 80 | implementation 'com.android.support:support-annotations:28.0.0' 81 | implementation 'android.arch.lifecycle:common:1.1.1' 82 | } 83 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 26 | 27 | 33 | 36 | 37 | 38 | 39 | 45 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/wuba/flutter/example/App.java: -------------------------------------------------------------------------------- 1 | package com.wuba.flutter.example; 2 | 3 | import com.idlefish.flutterboost.FlutterBoost; 4 | import com.idlefish.flutterboost.Platform; 5 | import com.idlefish.flutterboost.Utils; 6 | import com.idlefish.flutterboost.interfaces.INativeRouter; 7 | 8 | import io.flutter.app.FlutterApplication; 9 | import io.flutter.embedding.android.FlutterView; 10 | import io.flutter.plugins.GeneratedPluginRegistrant; 11 | 12 | public class App extends FlutterApplication { 13 | 14 | @Override 15 | public void onCreate() { 16 | super.onCreate(); 17 | 18 | init(); 19 | 20 | } 21 | 22 | private void init() { 23 | 24 | INativeRouter router = (context, url, urlParams, requestCode, exts) -> { 25 | String assembleUrl = Utils.assembleUrl(url, urlParams); 26 | PageRouter.openPageByUrl(context, assembleUrl, urlParams); 27 | }; 28 | 29 | FlutterBoost.BoostLifecycleListener boostLifecycleListener = new FlutterBoost.BoostLifecycleListener() { 30 | 31 | @Override 32 | public void beforeCreateEngine() { 33 | 34 | } 35 | 36 | @Override 37 | public void onEngineCreated() { 38 | //TODO:这个插件注册是干什么用的 39 | //ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(FlutterBoost.instance().engineProvider()); 40 | GeneratedPluginRegistrant.registerWith(FlutterBoost.instance().engineProvider()); 41 | // GeneratedPluginRegistrant.registerWith(FlutterBoost.instance().engineProvider().getPlugins()); 42 | // BinaryMessenger messenger = FlutterBoost.instance().engineProvider().getDartExecutor(); 43 | } 44 | 45 | @Override 46 | public void onPluginsRegistered() { 47 | 48 | } 49 | 50 | @Override 51 | public void onEngineDestroy() { 52 | 53 | } 54 | 55 | }; 56 | 57 | Platform platform = new FlutterBoost 58 | .ConfigBuilder(this, router) 59 | .isDebug(BuildConfig.DEBUG) 60 | .whenEngineStart(FlutterBoost.ConfigBuilder.ANY_ACTIVITY_CREATED) 61 | .renderMode(FlutterView.RenderMode.texture) 62 | .lifecycleListener(boostLifecycleListener) 63 | .build(); 64 | 65 | FlutterBoost.instance().init(platform); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/wuba/flutter/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.wuba.flutter.example; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.graphics.Color; 6 | import android.os.Build; 7 | import android.os.Bundle; 8 | import android.view.View; 9 | import android.view.Window; 10 | import android.view.WindowManager; 11 | 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | public class MainActivity extends Activity { 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setDeepStatusBar(true, this); 21 | setContentView(R.layout.activity_main); 22 | Map params = new HashMap(); 23 | params.put("test1", "v_test1"); 24 | params.put("test2", "v_test2"); 25 | 26 | findViewById(R.id.tv_normal).setOnClickListener(v -> 27 | startActivity(new Intent(MainActivity.this, NormalActivity.class))); 28 | 29 | findViewById(R.id.tv_flutter_boost).setOnClickListener(v -> 30 | PageRouter.openPageByUrl(MainActivity.this, PageRouter.FLUTTER_TOP_SCREEN, params)); 31 | 32 | findViewById(R.id.tv_flutter_boost_2).setOnClickListener(v -> 33 | PageRouter.openPageByUrl(MainActivity.this, PageRouter.FLUTTER_UNDER_SCREEN, params)); 34 | 35 | 36 | 37 | } 38 | 39 | 40 | public boolean setDeepStatusBar(boolean isChange, Activity mActivity) { 41 | if (!isChange) { 42 | return false; 43 | } 44 | 45 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 46 | // 透明状态栏 47 | Window window = mActivity.getWindow(); 48 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS 49 | | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); 50 | window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 51 | /*| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION*/ 52 | | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 53 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 54 | window.setStatusBarColor(Color.TRANSPARENT); 55 | 56 | //设置状态栏文字颜色及图标为深色 57 | mActivity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 58 | | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); 59 | 60 | return true; 61 | } else { 62 | return false; 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/wuba/flutter/example/NormalActivity.java: -------------------------------------------------------------------------------- 1 | package com.wuba.flutter.example; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.NonNull; 5 | import android.util.Log; 6 | import android.widget.Toast; 7 | 8 | import io.flutter.embedding.android.FlutterActivity; 9 | import io.flutter.embedding.engine.FlutterEngine; 10 | import io.flutter.plugins.GeneratedPluginRegistrant; 11 | import io.flutter.plugins.wuba.magpielog.MagpieLogListener; 12 | import io.flutter.plugins.wuba.magpielog.MagpieLogPlugin; 13 | 14 | public class NormalActivity extends FlutterActivity implements MagpieLogListener { 15 | 16 | @Override 17 | protected void onCreate(Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | } 20 | 21 | @Override 22 | public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { 23 | GeneratedPluginRegistrant.registerWith(flutterEngine); 24 | MagpieLogPlugin.getInstance().registerLogListener(this::magpieDataListener); 25 | } 26 | 27 | /** 28 | * 接收Flutter端圈选上报数据 29 | */ 30 | @Override 31 | public void magpieDataListener(String jsonData) { 32 | Toast.makeText(getApplicationContext(), "Native端收到了上报数据:" + jsonData, Toast.LENGTH_LONG).show(); 33 | Log.d("basicMessageChannel", "Native端收到了上报数据:" + jsonData); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/wuba/flutter/example/PageRouter.java: -------------------------------------------------------------------------------- 1 | package com.wuba.flutter.example; 2 | 3 | 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | 8 | import com.idlefish.flutterboost.containers.BoostFlutterActivity; 9 | 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | public class PageRouter { 14 | final static String FLUTTER_TOP_SCREEN = "magpieBoost://top_screen"; 15 | final static String FLUTTER_UNDER_SCREEN = "magpieBoost://under_screen"; 16 | public final static Map pageName = new HashMap() {{ 17 | put(FLUTTER_TOP_SCREEN, ""); 18 | put(FLUTTER_UNDER_SCREEN, ""); 19 | }}; 20 | 21 | 22 | public static boolean openPageByUrl(Context context, String url, Map params) { 23 | return openPageByUrl(context, url, params, 0); 24 | } 25 | 26 | public static boolean openPageByUrl(Context context, String url, Map params, int requestCode) { 27 | String path = url.split("\\?")[0]; 28 | try { 29 | if (pageName.containsKey(path)) { 30 | Intent intent = BoostFlutterActivity.withNewEngine().url(path).params(params) 31 | .backgroundMode(BoostFlutterActivity.BackgroundMode.opaque).build(context); 32 | if (context instanceof Activity) { 33 | Activity activity = (Activity) context; 34 | activity.startActivityForResult(intent, requestCode); 35 | } else { 36 | context.startActivity(intent); 37 | } 38 | return true; 39 | } else if (url.startsWith("native")) { 40 | } 41 | 42 | return false; 43 | 44 | } catch (Throwable t) { 45 | return false; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java: -------------------------------------------------------------------------------- 1 | package io.flutter.plugins; 2 | 3 | import android.support.annotation.Keep; 4 | import android.support.annotation.NonNull; 5 | import io.flutter.embedding.engine.FlutterEngine; 6 | import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry; 7 | 8 | /** 9 | * Generated file. Do not edit. 10 | * This file is generated by the Flutter tool based on the 11 | * plugins that support the Android platform. 12 | */ 13 | @Keep 14 | public final class GeneratedPluginRegistrant { 15 | public static void registerWith(@NonNull FlutterEngine flutterEngine) { 16 | ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine); 17 | flutterEngine.getPlugins().add(new io.flutter.plugins.deviceinfo.DeviceInfoPlugin()); 18 | com.idlefish.flutterboost.FlutterBoostPlugin.registerWith(shimPluginRegistry.registrarFor("com.idlefish.flutterboost.FlutterBoostPlugin")); 19 | io.github.ponnamkarthik.toast.fluttertoast.FluttertoastPlugin.registerWith(shimPluginRegistry.registrarFor("io.github.ponnamkarthik.toast.fluttertoast.FluttertoastPlugin")); 20 | io.flutter.plugins.wuba.magpielog.MagpieLogPlugin.registerWith(shimPluginRegistry.registrarFor("io.flutter.plugins.wuba.magpielog.MagpieLogPlugin")); 21 | flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 22 | 23 | 35 | 36 | 48 | 49 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/android/app/src/main/res/mipmap-xxhdpi/icon.jpg -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/android/app/src/main/res/mipmap-xxhdpi/logo.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } 4 | maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' } 5 | maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' } 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.2.1' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } 16 | maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' } 17 | maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' } 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | android.enableR8=true 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /example/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /example/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /example/android/local.properties: -------------------------------------------------------------------------------- 1 | sdk.dir=/Users/a58/Library/Android/sdk 2 | flutter.sdk=/Users/a58/flutter 3 | flutter.buildMode=debug 4 | flutter.versionName=1.0.0 5 | flutter.versionCode=1 -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/assets/analysis.json: -------------------------------------------------------------------------------- 1 | {"data":[{"actionName":"/","pagePath":"/","analysisData":"{\"otherState\":1,\"param1\":1}","description":"gggggg","type":"page"}],"reportChannel":0,"reportMethod":1} -------------------------------------------------------------------------------- /example/images/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/images/list.png -------------------------------------------------------------------------------- /example/images/list_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/images/list_selected.png -------------------------------------------------------------------------------- /example/images/page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/images/page.png -------------------------------------------------------------------------------- /example/images/page_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/images/page_selected.png -------------------------------------------------------------------------------- /example/images/redux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/images/redux.png -------------------------------------------------------------------------------- /example/images/redux_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/images/redux_selected.png -------------------------------------------------------------------------------- /example/images/state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/images/state.png -------------------------------------------------------------------------------- /example/images/state_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/images/state_selected.png -------------------------------------------------------------------------------- /example/images/verbose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/images/verbose.png -------------------------------------------------------------------------------- /example/images/verbose_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/images/verbose_selected.png -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Flutter.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Generated.xcconfig: -------------------------------------------------------------------------------- 1 | // This is a generated file; do not edit or check into version control. 2 | FLUTTER_ROOT=/Users/a58/flutter 3 | FLUTTER_APPLICATION_PATH=/Users/a58/flutterProject/gitRepo/magpie_log/example 4 | FLUTTER_TARGET=lib/main.dart 5 | FLUTTER_BUILD_DIR=build 6 | SYMROOT=${SOURCE_ROOT}/../build/ios 7 | FLUTTER_FRAMEWORK_DIR=/Users/a58/flutter/bin/cache/artifacts/engine/ios 8 | FLUTTER_BUILD_NAME=1.0.0 9 | FLUTTER_BUILD_NUMBER=1 10 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Flutter.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | #import 5 | 6 | @implementation AppDelegate 7 | 8 | - (BOOL)application:(UIApplication *)application 9 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 10 | [GeneratedPluginRegistrant registerWithRegistry:self]; 11 | // Override point for customization after application launch. 12 | [MagpieLogPlugin setLogHandler:^(NSString * log) { 13 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"DartLog" message:log preferredStyle:UIAlertControllerStyleAlert]; 14 | [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:nil]]; 15 | [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated:NO completion:nil]; 16 | NSLog(@"native log: %@",log); 17 | }]; 18 | 19 | 20 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 21 | } 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/GeneratedPluginRegistrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | #ifndef GeneratedPluginRegistrant_h 6 | #define GeneratedPluginRegistrant_h 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface GeneratedPluginRegistrant : NSObject 13 | + (void)registerWithRegistry:(NSObject*)registry; 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | #endif /* GeneratedPluginRegistrant_h */ 18 | -------------------------------------------------------------------------------- /example/ios/Runner/GeneratedPluginRegistrant.m: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | #import "GeneratedPluginRegistrant.h" 6 | 7 | #if __has_include() 8 | #import 9 | #else 10 | @import device_info; 11 | #endif 12 | 13 | #if __has_include() 14 | #import 15 | #else 16 | @import flutter_boost; 17 | #endif 18 | 19 | #if __has_include() 20 | #import 21 | #else 22 | @import fluttertoast; 23 | #endif 24 | 25 | #if __has_include() 26 | #import 27 | #else 28 | @import magpie_log; 29 | #endif 30 | 31 | #if __has_include() 32 | #import 33 | #else 34 | @import path_provider; 35 | #endif 36 | 37 | @implementation GeneratedPluginRegistrant 38 | 39 | + (void)registerWithRegistry:(NSObject*)registry { 40 | [FLTDeviceInfoPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTDeviceInfoPlugin"]]; 41 | [FlutterBoostPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterBoostPlugin"]]; 42 | [FluttertoastPlugin registerWithRegistrar:[registry registrarForPlugin:@"FluttertoastPlugin"]]; 43 | [MagpieLogPlugin registerWithRegistrar:[registry registrarForPlugin:@"MagpieLogPlugin"]]; 44 | [FLTPathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTPathProviderPlugin"]]; 45 | } 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/lib/flutterboost/main_flutter_boost.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/flutterboost/page_router.dart'; 2 | import 'package:example/states/app_state.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_boost/container/boost_page_route.dart'; 5 | import 'package:flutter_boost/flutter_boost.dart'; 6 | import 'package:flutter_redux/flutter_redux.dart'; 7 | import 'package:magpie_log/interceptor/intercepter_screen_log.dart'; 8 | import 'package:magpie_log/interceptor/interceptor_circle_log.dart'; 9 | import 'package:magpie_log/magpie_log.dart'; 10 | import 'package:magpie_log/model/analysis_model.dart'; 11 | import 'package:redux/redux.dart'; 12 | 13 | void main() { 14 | //1.在main函数初始化store中加入CircleMiddleWare中间件拦截action实践,用于圈选拦截(和普通路由一样) 15 | final store = Store(reducer, 16 | middleware: [CircleMiddleWare()], initialState: AppState.initState()); 17 | runApp(MyApp(store)); 18 | } 19 | 20 | class MyApp extends StatelessWidget { 21 | final Store store; 22 | 23 | MyApp(this.store); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | //2.初始化MagpieLog pageNameCallback回调 flutterBoost的路由id和普通navigator跳转位置不通 28 | MagpieLog.instance.init(context, ReportMethod.timing, ReportChannel.natives, 29 | time: 1 * 60 * 1000, count: 3, pageNameCallback: (Route route) { 30 | if (route is BoostPageRoute) { 31 | return route.pageName; 32 | } 33 | return ""; 34 | }); 35 | 36 | //3.设置监听的方式和普通方式不同 要在addBoostNavigatorObserver设置否则监听不到flutterBoost的路由跳转 37 | FlutterBoost.singleton.addBoostNavigatorObserver(LogObserver()); 38 | 39 | PageRouter.init(); 40 | return StoreProvider( 41 | store: store, 42 | child: MaterialApp( 43 | builder: FlutterBoost.init(), 44 | home: Container(), 45 | theme: ThemeData(primarySwatch: Colors.deepOrange), 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example/lib/flutterboost/page_router.dart: -------------------------------------------------------------------------------- 1 | //flutter 页面 2 | import 'package:example/top_screen.dart'; 3 | import 'package:example/under_screen.dart'; 4 | import 'package:flutter_boost/flutter_boost.dart'; 5 | 6 | //flutter 页面 7 | const String FLUTTER_TOP_SCREEN = "magpieBoost://top_screen"; 8 | const String FLUTTER_UNDER_SCREEN = "magpieBoost://under_screen"; 9 | 10 | //native 页面 11 | const String NATIVE_ = "magpieBoost://"; 12 | 13 | class PageRouter { 14 | static init() { 15 | FlutterBoost.singleton.registerPageBuilders({ 16 | FLUTTER_TOP_SCREEN: (pageName, params, _) { 17 | return TopScreen(); 18 | }, 19 | FLUTTER_UNDER_SCREEN: (pageName, params, _) { 20 | return UnderScreen(); 21 | }, 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_redux/flutter_redux.dart'; 3 | import 'package:magpie_log/interceptor/intercepter_screen_log.dart'; 4 | import 'package:magpie_log/interceptor/interceptor_circle_log.dart'; 5 | import 'package:magpie_log/magpie_log.dart'; 6 | import 'package:magpie_log/model/analysis_model.dart'; 7 | import 'package:redux/redux.dart'; 8 | 9 | import 'states/app_state.dart'; 10 | import 'top_screen.dart'; 11 | import 'under_screen.dart'; 12 | 13 | void main() { 14 | //1.在main函数初始化store中加入CircleMiddleWare中间件拦截action实践,用于圈选拦截 15 | final store = Store(reducer, 16 | middleware: [CircleMiddleWare()], initialState: AppState.initState()); 17 | runApp(MyApp(store)); 18 | } 19 | 20 | class MyApp extends StatelessWidget { 21 | final Store store; 22 | 23 | MyApp(this.store); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | //2.MagpieLog初始化 28 | MagpieLog.instance.init(context, ReportMethod.timing, ReportChannel.natives, 29 | time: 1 * 60 * 1000, count: 3); 30 | 31 | return StoreProvider( 32 | store: store, 33 | child: MaterialApp( 34 | routes: { 35 | '/': (BuildContext context) => TopScreen(), 36 | '/UnderScreen': (BuildContext context) => UnderScreen(), 37 | }, 38 | //3.添加路由跳转监听,用于页面曝光拦截,以及路由堆栈建立,获取当前action唯一标识 39 | navigatorObservers: [ 40 | LogObserver(), 41 | ], 42 | title: 'Flutter Demo', 43 | theme: ThemeData(primarySwatch: Colors.deepOrange), 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/lib/states/app_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:magpie_log/magpie_log.dart'; 3 | 4 | part 'app_state.g.dart'; 5 | 6 | ///State中所有属性都应该是只读的 7 | 8 | @JsonSerializable() 9 | class CountState { 10 | final int count; 11 | final String param1; 12 | final int param2; 13 | int param3; 14 | 15 | CountState(this.count, {this.param1, this.param2}); 16 | 17 | CountState.initState({this.param1, this.param2}) : count = 0; 18 | 19 | Map toJson() => _$CountStateToJson(this); 20 | 21 | factory CountState.fromJson(Map json) => 22 | _$CountStateFromJson(json); 23 | } 24 | 25 | @JsonSerializable(explicitToJson: true) 26 | class AppState extends LogState { 27 | final CountState countState; 28 | final List listState; 29 | final OtherState otherState; 30 | final String param1; 31 | final int param2; 32 | 33 | AppState( 34 | {this.countState, 35 | this.listState, 36 | this.otherState, 37 | this.param1, 38 | this.param2}); 39 | 40 | AppState.initState({this.param1, this.param2, this.otherState}) 41 | : countState = CountState.initState(), 42 | listState = initListState(); 43 | 44 | Map toJson() => _$AppStateToJson(this); 45 | 46 | factory AppState.fromJson(Map json) => 47 | _$AppStateFromJson(json); 48 | } 49 | 50 | List initListState() { 51 | List list = []; 52 | for (int i = 0; i < 20; i++) { 53 | list.add(ListItem( 54 | title: "Title:" + i.toString(), content: "Content:" + i.toString())); 55 | } 56 | return list; 57 | } 58 | 59 | @JsonSerializable() 60 | class ListItem { 61 | final String title; 62 | final String content; 63 | final bool isChecked; 64 | 65 | ListItem({this.title, this.content, this.isChecked = false}); 66 | 67 | Map toJson() => _$ListItemToJson(this); 68 | 69 | factory ListItem.fromJson(Map json) => 70 | _$ListItemFromJson(json); 71 | } 72 | 73 | @JsonSerializable() 74 | class OtherState { 75 | final String param1; 76 | final int param2; 77 | 78 | OtherState(this.param1, this.param2); 79 | 80 | Map toJson() => _$OtherStateToJson(this); 81 | 82 | factory OtherState.fromJson(Map json) => 83 | _$OtherStateFromJson(json); 84 | } 85 | 86 | const String actionAddCount = "addCount"; 87 | const String actionListClick = "listClick"; 88 | 89 | ///reducer会根据传进来的action生成新的CountState 90 | AppState reducer(AppState state, action) { 91 | //匹配Action 92 | 93 | if (action is LogAction && action.actionName == actionAddCount) { 94 | return AppState( 95 | countState: CountState(state.countState.count + 1), 96 | listState: state.listState); 97 | } else if (action is LogAction && action.actionName == actionListClick) { 98 | ListItem listItem = state.listState[action.index]; 99 | 100 | state.listState[action.index] = ListItem( 101 | title: listItem.title, 102 | content: listItem.content, 103 | isChecked: !listItem.isChecked); 104 | } 105 | return state; 106 | } 107 | -------------------------------------------------------------------------------- /example/lib/states/app_state.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'app_state.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | CountState _$CountStateFromJson(Map json) { 10 | return CountState( 11 | json['count'] as int, 12 | param1: json['param1'] as String, 13 | param2: json['param2'] as int, 14 | )..param3 = json['param3'] as int; 15 | } 16 | 17 | Map _$CountStateToJson(CountState instance) => 18 | { 19 | 'count': instance.count, 20 | 'param1': instance.param1, 21 | 'param2': instance.param2, 22 | 'param3': instance.param3, 23 | }; 24 | 25 | AppState _$AppStateFromJson(Map json) { 26 | return AppState( 27 | countState: json['countState'] == null 28 | ? null 29 | : CountState.fromJson(json['countState'] as Map), 30 | listState: (json['listState'] as List) 31 | ?.map((e) => 32 | e == null ? null : ListItem.fromJson(e as Map)) 33 | ?.toList(), 34 | otherState: json['otherState'] == null 35 | ? null 36 | : OtherState.fromJson(json['otherState'] as Map), 37 | param1: json['param1'] as String, 38 | param2: json['param2'] as int, 39 | ); 40 | } 41 | 42 | Map _$AppStateToJson(AppState instance) => { 43 | 'countState': instance.countState?.toJson(), 44 | 'listState': instance.listState?.map((e) => e?.toJson())?.toList(), 45 | 'otherState': instance.otherState?.toJson(), 46 | 'param1': instance.param1, 47 | 'param2': instance.param2, 48 | }; 49 | 50 | ListItem _$ListItemFromJson(Map json) { 51 | return ListItem( 52 | title: json['title'] as String, 53 | content: json['content'] as String, 54 | isChecked: json['isChecked'] as bool, 55 | ); 56 | } 57 | 58 | Map _$ListItemToJson(ListItem instance) => { 59 | 'title': instance.title, 60 | 'content': instance.content, 61 | 'isChecked': instance.isChecked, 62 | }; 63 | 64 | OtherState _$OtherStateFromJson(Map json) { 65 | return OtherState( 66 | json['param1'] as String, 67 | json['param2'] as int, 68 | ); 69 | } 70 | 71 | Map _$OtherStateToJson(OtherState instance) => 72 | { 73 | 'param1': instance.param1, 74 | 'param2': instance.param2, 75 | }; 76 | -------------------------------------------------------------------------------- /example/lib/top_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_redux/flutter_redux.dart'; 3 | import 'package:magpie_log/handler/statistics_handler.dart'; 4 | import 'package:magpie_log/interceptor/interceptor_state_log.dart'; 5 | import 'package:magpie_log/magpie_log.dart'; 6 | import 'package:redux/redux.dart'; 7 | 8 | import 'states/app_state.dart'; 9 | 10 | class TopScreen extends StatefulWidget { 11 | @override 12 | _TopScreenState createState() => _TopScreenState(); 13 | } 14 | 15 | class _TopScreenState extends State { 16 | int index = 0; 17 | @override 18 | Widget build(BuildContext context) { 19 | return DefaultTabController( 20 | length: choices.length, 21 | child: Scaffold( 22 | appBar: AppBar( 23 | title: Text("Magpie-Log"), 24 | centerTitle: true, 25 | bottom: TabBar( 26 | isScrollable: false, 27 | tabs: choices.map((Choice choice) { 28 | return Tab( 29 | child: Text( 30 | '${choice.title}', 31 | style: TextStyle(fontSize: 10), 32 | ), 33 | icon: Image.asset(choice.icon, height: 30, width: 30), 34 | ); 35 | }).toList(), 36 | )), 37 | body: TabBarView(children: [ 38 | reduxDemo(context), 39 | listDemo(context), 40 | stateDemo(context), 41 | manuallyDemo(context), 42 | pageDemo(context), 43 | ])), 44 | ); 45 | } 46 | } 47 | 48 | class Choice { 49 | const Choice({this.title, this.icon}); 50 | 51 | final String title; 52 | final String icon; 53 | } 54 | 55 | const List choices = const [ 56 | const Choice(title: 'Redux', icon: "images/redux.png"), 57 | const Choice(title: 'List', icon: "images/list.png"), 58 | const Choice(title: 'State', icon: "images/state.png"), 59 | const Choice(title: 'Verbose', icon: "images/verbose.png"), 60 | const Choice(title: 'Page', icon: "images/page.png"), 61 | ]; 62 | 63 | Widget demo(context, title, demoView, {description}) { 64 | return Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ 65 | Padding( 66 | padding: EdgeInsets.fromLTRB(15, 25, 15, 20), 67 | child: Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ 68 | Text( 69 | "\n" + title, 70 | style: TextStyle(fontSize: 18, color: Colors.black), 71 | ), 72 | Text( 73 | description != null ? "\n" + description : "", 74 | style: TextStyle(fontSize: 14, color: Colors.black54), 75 | ) 76 | ])), 77 | Container( 78 | color: Color(0xFFf6f7fb), 79 | width: MediaQuery.of(context).size.width, 80 | child: Padding( 81 | padding: EdgeInsets.fromLTRB(15, 15, 15, 5), 82 | child: Text( 83 | "示例:", 84 | style: TextStyle(fontSize: 14, color: Colors.black54), 85 | )), 86 | ), 87 | demoView, 88 | ]); 89 | } 90 | 91 | Widget reduxDemo(context) { 92 | return demo( 93 | context, 94 | "基于Redux的普通圈选展示", 95 | Padding( 96 | padding: EdgeInsets.fromLTRB(15, 15, 15, 5), 97 | child: 98 | Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ 99 | StoreConnector( 100 | converter: (store) => store.state.countState.count, 101 | builder: (context, count) { 102 | return Text( 103 | count.toString(), 104 | style: TextStyle(fontSize: 30, color: Colors.red), 105 | ); 106 | }, 107 | ), 108 | Container( 109 | margin: EdgeInsets.all(15), 110 | child: StoreConnector( 111 | converter: (store) { 112 | return () => store.dispatch(LogAction(actionAddCount)); 113 | }, 114 | builder: (context, callback) { 115 | return MaterialButton( 116 | child: Text("数字+1", 117 | style: 118 | TextStyle(fontSize: 15, color: Colors.deepOrange)), 119 | onPressed: callback, 120 | color: Colors.white, 121 | shape: Border.all( 122 | color: Colors.deepOrange, 123 | width: 2, 124 | style: BorderStyle.solid), 125 | ); 126 | }, 127 | ), 128 | ), 129 | ])), 130 | description: "原理:拦截redux分发action事件,插入中间件,进行统一埋点"); 131 | } 132 | 133 | Widget listDemo(context) { 134 | return demo( 135 | context, 136 | "基于Redux的List圈选展示", 137 | Expanded( 138 | child: StoreConnector>( 139 | converter: (store) => store, 140 | builder: (context, store) { 141 | return ListView.separated( 142 | itemBuilder: (BuildContext context, int index) { 143 | return new ListTile( 144 | selected: store.state.listState[index].isChecked, 145 | onTap: () { 146 | store.dispatch( 147 | LogAction(actionListClick, index: index)); 148 | }, 149 | title: Center( 150 | child: Text(store.state.listState[index].title, 151 | style: store.state.listState[index].isChecked 152 | ? TextStyle( 153 | fontSize: 14, color: Colors.deepOrange) 154 | : TextStyle( 155 | fontSize: 14, color: Colors.black54)))); 156 | }, 157 | separatorBuilder: (context, index) { 158 | return Padding( 159 | padding: EdgeInsets.only(left: 0, right: 0), 160 | child: Divider(height: 1)); 161 | }, 162 | itemCount: store.state.listState.length); 163 | })), 164 | description: "原理:redux圈选一样,说明一下处理list的圈选数据取的item里面bean值", 165 | ); 166 | } 167 | 168 | Widget stateDemo(context) { 169 | return demo( 170 | context, 171 | "局部setState()操作圈选", 172 | Container(padding: EdgeInsets.all(15), child: AddTextWidget()), 173 | description: "原理:通过使用LogState,拦截setState事件进行统一埋点", 174 | ); 175 | } 176 | 177 | Widget manuallyDemo(context) { 178 | return demo( 179 | context, 180 | "手动埋点展示", 181 | Container( 182 | margin: EdgeInsets.all(15), 183 | child: MaterialButton( 184 | color: Colors.white, 185 | shape: Border.all( 186 | color: Colors.deepOrange, width: 2, style: BorderStyle.solid), 187 | child: Text('手动埋点', 188 | style: TextStyle(fontSize: 15, color: Colors.deepOrange)), 189 | onPressed: () { 190 | // 手动埋点数据示例 191 | MagpieStatisticsHandler.instance.writeData({'data': '手动埋点数据示例'}); 192 | }, 193 | ), 194 | )); 195 | } 196 | 197 | Widget pageDemo(BuildContext context) { 198 | return demo( 199 | context, 200 | "页面级曝光统计", 201 | Container( 202 | margin: EdgeInsets.all(15), 203 | child: MaterialButton( 204 | color: Colors.white, 205 | shape: Border.all( 206 | color: Colors.deepOrange, width: 2, style: BorderStyle.solid), 207 | child: Text("跳转", 208 | style: TextStyle(fontSize: 15, color: Colors.deepOrange)), 209 | onPressed: () { 210 | Navigator.pushNamed(context, '/UnderScreen'); 211 | }, 212 | )), 213 | description: "原理:通过过NavigatorObserver监听页面push事件,现push页面后 默认0.5秒跳转圈选部分"); 214 | } 215 | 216 | class AddTextWidget extends StatefulWidget { 217 | @override 218 | State createState() { 219 | return AddTextState(); 220 | } 221 | } 222 | 223 | class AddTextState extends WidgetLogState { 224 | int count; 225 | 226 | @override 227 | void initState() { 228 | count = 0; 229 | super.initState(); 230 | } 231 | 232 | @override 233 | Widget onBuild(BuildContext context) { 234 | return Column( 235 | crossAxisAlignment: CrossAxisAlignment.center, 236 | children: [ 237 | Text( 238 | count.toString(), 239 | style: TextStyle(fontSize: 30, color: Colors.red), 240 | ), 241 | Container( 242 | margin: EdgeInsets.all(15), 243 | child: MaterialButton( 244 | color: Colors.white, 245 | shape: Border.all( 246 | color: Colors.deepOrange, 247 | width: 2, 248 | style: BorderStyle.solid, 249 | ), 250 | child: Text("数字+1", 251 | style: TextStyle(fontSize: 15, color: Colors.deepOrange)), 252 | onPressed: () { 253 | setState(() { 254 | count++; 255 | }); 256 | }, 257 | ), 258 | ), 259 | ], 260 | ); 261 | } 262 | 263 | @override 264 | Map toJson() { 265 | Map map = Map(); 266 | map["count"] = count.toString(); 267 | return map; 268 | } 269 | 270 | @override 271 | String getActionName() { 272 | return "AddText"; 273 | } 274 | 275 | @override 276 | int getIndex() { 277 | return 0; 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /example/lib/under_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class UnderScreen extends StatefulWidget { 4 | @override 5 | _UnderScreenState createState() => _UnderScreenState(); 6 | } 7 | 8 | class _UnderScreenState extends State { 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: AppBar( 13 | title: Text('跳转页面'), 14 | ), 15 | body: Center( 16 | child: Column( 17 | mainAxisAlignment: MainAxisAlignment.center, 18 | children: [ 19 | Text( 20 | '跳转页面默认0.5秒后跳转圈选埋点页面', 21 | ), 22 | ], 23 | ), 24 | ), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/lib/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fluttertoast/fluttertoast.dart'; 3 | import 'package:magpie_log/magpie_log.dart'; 4 | import 'package:magpie_log/model/analysis_model.dart'; 5 | 6 | class MagpieExampleUtils { 7 | final String tag = 'MagpieExampleUtils'; 8 | 9 | factory MagpieExampleUtils() => _getInstance(); 10 | 11 | MagpieExampleUtils get instance => _getInstance(); 12 | 13 | static MagpieExampleUtils _instance; 14 | 15 | MagpieExampleUtils._init(); 16 | 17 | static MagpieExampleUtils _getInstance() { 18 | if (_instance == null) { 19 | _instance = MagpieExampleUtils._init(); 20 | } 21 | 22 | return _instance; 23 | } 24 | 25 | void init(BuildContext context) { 26 | MagpieLog.instance.init(context, ReportMethod.timing, ReportChannel.flutter, 27 | callback: _receiverMagpieData, time: 1 * 60 * 1000, count: 3); 28 | } 29 | 30 | void _receiverMagpieData(String jsonData) { 31 | Fluttertoast.showToast( 32 | msg: "sendMsgToFlutter==>\n$jsonData", 33 | toastLength: Toast.LENGTH_LONG, 34 | gravity: ToastGravity.BOTTOM, 35 | timeInSecForIos: 1, 36 | backgroundColor: Colors.deepOrange, 37 | textColor: Colors.white, 38 | fontSize: 16.0); 39 | print('basicMessageChannel $jsonData'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A new Flutter example 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.1.0 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | magpie_log: 23 | path: ../ 24 | flutter_boost: 25 | git: 26 | url: 'https://github.com/alibaba/flutter_boost.git' 27 | ref: 'task/task_v1.12.13_support_hotfixes' 28 | json_annotation: ^3.0.0 29 | # The following adds the Cupertino Icons font to your application. 30 | # Use with the CupertinoIcons class for iOS style icons. 31 | 32 | dev_dependencies: 33 | flutter_test: 34 | sdk: flutter 35 | build_runner: ^1.0.0 36 | json_serializable: ^3.2.3 37 | # For information on the generic Dart part of this file, see the 38 | # following page: https://dart.dev/tools/pub/pubspec 39 | 40 | # The following section is specific to Flutter. 41 | flutter: 42 | # The following line ensures that the Material Icons font is 43 | # included with your application, so that you can use the icons in 44 | # the material Icons class. 45 | uses-material-design: true 46 | 47 | assets: 48 | - assets/analysis.json 49 | - images/list.png 50 | - images/list_selected.png 51 | - images/page.png 52 | - images/page_selected.png 53 | - images/redux.png 54 | - images/redux_selected.png 55 | - images/state.png 56 | - images/state_selected.png 57 | - images/verbose.png 58 | - images/verbose_selected.png 59 | # To add assets to your application, add an assets section, like this: 60 | # assets: 61 | # - images/a_dot_burr.jpeg 62 | # - images/a_dot_ham.jpeg 63 | # An image asset can refer to one or more resolution-specific "variants", see 64 | # https://flutter.dev/assets-and-images/#resolution-aware. 65 | # For details regarding adding assets from package dependencies, see 66 | # https://flutter.dev/assets-and-images/#from-packages 67 | # To add custom fonts to your application, add a fonts section here, 68 | # in this "flutter" section. Each entry in this list should have a 69 | # "family" key with the font family name, and a "fonts" key with a 70 | # list giving the asset and other descriptors for the font. For 71 | # example: 72 | # fonts: 73 | # - family: Schyler 74 | # fonts: 75 | # - asset: fonts/Schyler-Regular.ttf 76 | # - asset: fonts/Schyler-Italic.ttf 77 | # style: italic 78 | # - family: Trajan Pro 79 | # fonts: 80 | # - asset: fonts/TrajanPro.ttf 81 | # - asset: fonts/TrajanPro_Bold.ttf 82 | # weight: 700 83 | # 84 | # For details regarding fonts from package dependencies, 85 | # see https://flutter.dev/custom-fonts/#from-packages 86 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /images/check_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/images/check_icon.png -------------------------------------------------------------------------------- /images/uncheck_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/magpie_log/2220b67e53b1429e422cf9eeebfd3b045ae964ac/images/uncheck_icon.png -------------------------------------------------------------------------------- /ios/Classes/MagpieLogPlugin.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | 4 | typedef void (^MagpieLogVoidCallBack) (NSString * log); 5 | 6 | @interface MagpieLogPlugin : NSObject 7 | 8 | + (void)setLogHandler:(MagpieLogVoidCallBack)handler; 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /ios/Classes/MagpieLogPlugin.m: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #import "MagpieLogPlugin.h" 6 | 7 | @interface MagpieLogPlugin () 8 | 9 | @property (nonatomic, copy) MagpieLogVoidCallBack callback; 10 | 11 | @end 12 | 13 | @implementation MagpieLogPlugin 14 | 15 | + (instancetype)sharedInstance{ 16 | static MagpieLogPlugin * magpie_log; 17 | static dispatch_once_t onceToken; 18 | dispatch_once(&onceToken, ^{ 19 | magpie_log = [[MagpieLogPlugin alloc] init]; 20 | }); 21 | return magpie_log; 22 | } 23 | 24 | + (void)registerWithRegistrar:(NSObject*)registrar { 25 | FlutterBasicMessageChannel* msgChannel = [FlutterBasicMessageChannel messageChannelWithName:@"magpie_analysis_channel" binaryMessenger:[registrar messenger] codec:[FlutterStringCodec new]]; 26 | [msgChannel setMessageHandler:^(id _Nullable message, FlutterReply _Nonnull callback){ 27 | if (MagpieLogPlugin.sharedInstance.callback) { 28 | MagpieLogPlugin.sharedInstance.callback(message); 29 | } 30 | }]; 31 | } 32 | 33 | 34 | + (void)setLogHandler:(MagpieLogVoidCallBack)handler{ 35 | if (handler) { 36 | MagpieLogPlugin.sharedInstance.callback = handler; 37 | }else{ 38 | MagpieLogPlugin.sharedInstance.callback = nil; 39 | } 40 | } 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /ios/magpie_log.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 3 | # 4 | Pod::Spec.new do |s| 5 | s.name = 'magpie_log' 6 | s.version = '0.0.1' 7 | s.summary = 'Flutter plugin for magpie_log' 8 | s.description = <<-DESC 9 | magpie_log 10 | DESC 11 | s.homepage = 'https://github.com/' 12 | s.license = { :file => '../LICENSE' } 13 | s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } 14 | s.source = { :path => '.' } 15 | s.source_files = 'Classes/**/*' 16 | s.public_header_files = 'Classes/**/*.h' 17 | s.dependency 'Flutter' 18 | 19 | s.platform = :ios, '8.0' 20 | end 21 | -------------------------------------------------------------------------------- /lib/file/data_analysis.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:device_info/device_info.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:magpie_log/file/file_utils.dart'; 7 | import 'package:magpie_log/handler/statistics_handler.dart'; 8 | import 'package:magpie_log/model/analysis_model.dart'; 9 | import 'package:magpie_log/model/device_data.dart'; 10 | 11 | import '../magpie_log.dart'; 12 | 13 | ///数据分析配置文件操作 14 | class MagpieDataAnalysis { 15 | static final String _tag = 'Magpie Data Analysis'; 16 | 17 | static final String _dirName = 'data_analysis'; 18 | 19 | ///数据分析配置参数统一写到统一文件中,所以在此直接定义好文件名称 20 | static final String _fileName = 'analysis.json'; 21 | 22 | static final List _listData = List(); 23 | 24 | /// 初始化接口 25 | static void initMagpieData(BuildContext context) async { 26 | var data; 27 | //圈选数据以文件中的为准,只有首次的时候从assets下读取并copy到内存中 28 | //动态下发的埋点数据需要全部写入到文件中 29 | if (await MagpieFileUtils.isExistsFile( 30 | fileName: _fileName, dirName: _dirName)) { 31 | data = await MagpieFileUtils.readFile( 32 | fileName: _fileName, dirName: _dirName); 33 | } else { 34 | //原则上assets目录中的配置文件只会读取一次 35 | data = await DefaultAssetBundle.of(context) 36 | .loadString('assets/analysis.json'); 37 | } 38 | 39 | if (_listData.isEmpty) { 40 | if (data.isNotEmpty) { 41 | Map analysis = jsonDecode(data); 42 | AnalysisData analysisData = AnalysisData.fromJson(analysis); 43 | _listData.addAll(analysisData.data); 44 | print( 45 | '$_tag initMagpieData, ${analysisData.reportChannel} , ${analysisData.reportMethod}'); 46 | } 47 | } 48 | } 49 | 50 | static Future saveData() async { 51 | if (_listData.isEmpty) { 52 | print('$_tag saveData error!!! _listData is empty...'); 53 | return; 54 | } 55 | //判断是否有之前写入的文件,有则删除 56 | await MagpieFileUtils.rmFile(fileName: _fileName, dirName: _dirName); 57 | 58 | await MagpieFileUtils.writeFile( 59 | fileName: _fileName, 60 | contents: jsonEncode(AnalysisData( 61 | _listData, 62 | MagpieStatisticsHandler.instance.reportChannel, 63 | MagpieStatisticsHandler.instance.reportMethod) 64 | .toJson()), 65 | dirName: _dirName); 66 | } 67 | 68 | static Future writeData(AnalysisModel analysisModel) async { 69 | if (analysisModel == null || 70 | analysisModel.actionName.isEmpty || 71 | analysisModel.pagePath.isEmpty || 72 | analysisModel.analysisData.isEmpty) { 73 | print('$_tag writeData error!!! 请再次检查AnalysisModel!!! '); 74 | return; 75 | } 76 | 77 | if (_listData.isEmpty) { 78 | //首次添加先获取全量数据 79 | String analysisData = await readFileData(); 80 | if (analysisData.isNotEmpty) { 81 | Map analysis = jsonDecode(analysisData); 82 | AnalysisData data = AnalysisData.fromJson(analysis); 83 | _listData.addAll(data.data); 84 | print('$_tag writeData addAll list length = ${_listData.length}'); 85 | } 86 | } 87 | //数据去重 88 | if (_listData.any((item) => item.actionName == analysisModel.actionName)) { 89 | print( 90 | '$_tag writeData list replace data, action = ${analysisModel.toString()} '); 91 | for (var item in _listData) { 92 | if (item.actionName == analysisModel.actionName && 93 | item.pagePath == analysisModel.pagePath) { 94 | _listData.remove(item); 95 | _listData.add(analysisModel); 96 | print( 97 | '$_tag writeData list replace data, action = ${analysisModel.actionName}} : pagePath = ${analysisModel.pagePath} : data = ${item.analysisData}'); 98 | } 99 | } 100 | } else { 101 | print('$_tag writeData list add data'); 102 | _listData.add(analysisModel); 103 | } 104 | 105 | print( 106 | '$_tag writeData list length = ${_listData.length}, action = ${analysisModel.actionName} : pagePath = ${analysisModel.pagePath} : data = ${analysisModel.analysisData}'); 107 | } 108 | 109 | ///完整的圈选数据读取 110 | static Future readFileData() async { 111 | try { 112 | return jsonEncode(AnalysisData( 113 | _listData, 114 | MagpieStatisticsHandler.instance.reportChannel, 115 | MagpieStatisticsHandler.instance.reportMethod)); 116 | } catch (e) { 117 | print('$_tag : readFile error = $e'); 118 | } 119 | return ''; 120 | } 121 | 122 | ///获取已选择的圈选数据集合 123 | static List getListData() => _listData; 124 | 125 | ///根据圈选埋点的action,读取指定数据。[action] 圈选埋点的key。 126 | static Future readActionData( 127 | {@required String actionName, 128 | @required String pagePath, 129 | String type}) async { 130 | if (_listData.isEmpty) { 131 | print('$_tag readActionData listData isEmpty!!!'); 132 | return null; 133 | } else if (actionName.isEmpty || pagePath.isEmpty) { 134 | print('$_tag readActionData 请检查传入参数是否正确!!!'); 135 | return null; 136 | } else { 137 | // if (_listData.any((item) => 138 | // item.actionName == actionName && item.pagePath == pagePath)) { 139 | 140 | List modelList = []; 141 | 142 | for (var item in _listData) { 143 | print( 144 | '$_tag readActionData actionName = ${item.actionName} , pagePath = ${item.pagePath} ,data = ${item.analysisData}'); 145 | if (item.actionName == actionName && item.pagePath == pagePath) { 146 | print( 147 | '$_tag readActionData select actionName = ${item.actionName} , pagePath = ${item.pagePath} ,data = ${item.analysisData}'); 148 | modelList.add(item); 149 | } 150 | } 151 | 152 | if (null != type) { 153 | for (var item in modelList) { 154 | if (item.type == type) { 155 | return item; 156 | } 157 | } 158 | } else if (modelList.length > 0) { 159 | return modelList[0]; 160 | } 161 | 162 | // } 163 | print( 164 | '$_tag readActionData listData none , actionName = $actionName , pagePath = $pagePath'); 165 | return null; 166 | } 167 | } 168 | 169 | ///获取文件路径 170 | static Future getSavePath() async { 171 | return await MagpieFileUtils.getFilePath( 172 | fileName: _fileName, dirName: _dirName); 173 | } 174 | 175 | ///清除全部数据。直接删除文件就完了呀呀呀呀呀 176 | static Future clearAnalysisData() async { 177 | _listData.clear(); 178 | MagpieFileUtils.clearFileData(fileName: _fileName, dirName: _dirName); 179 | print('$_tag clearAnalysisData _listData.length = ${_listData.length}'); 180 | } 181 | 182 | static Future deleteActionData({@required String actionName}) async { 183 | if (actionName.isEmpty) { 184 | print('$_tag deleteActionData actionName isEmpty'); 185 | return -1; 186 | } 187 | 188 | if (_listData.isEmpty) { 189 | print('$_tag deleteActionData listData isEmpty'); 190 | return -3; 191 | } 192 | if (_listData.any((item) => item.actionName == actionName)) { 193 | for (var item in _listData) { 194 | if (item.actionName == actionName) { 195 | _listData.remove(item); 196 | 197 | print( 198 | '$_tag deleteActionData remove actionName = ${item.actionName}'); 199 | return 0; 200 | } 201 | } 202 | } 203 | 204 | return -2; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /lib/file/data_statistics.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'file_utils.dart'; 4 | 5 | class MagpieDataStatistics { 6 | static final String _tag = 'Magpie Data Statistics'; 7 | 8 | static final String _dirName = 'data_analysis'; 9 | 10 | ///数据分析配置参数统一写到统一文件中,所以在此直接定义好文件名称 11 | static final String _fileName = 'statistics.json'; 12 | 13 | static Future writeStatisticsData(Map data) async { 14 | if (data != null && data.isNotEmpty) { 15 | String json = jsonEncode(data); 16 | if (await isExistsStatistics()) { 17 | String fileData = await readStatisticsData(); 18 | clearStatisticsData(); 19 | if (fileData.endsWith(']}')) { 20 | String jsonData = 21 | fileData.substring(0, fileData.length - 2) + ',' + json + ']}'; 22 | await MagpieFileUtils.writeFile( 23 | fileName: _fileName, contents: jsonData, dirName: _dirName); 24 | print('$_tag writeStatisticsData data = $jsonData'); 25 | } else { 26 | await MagpieFileUtils.writeFile( 27 | fileName: _fileName, contents: json, dirName: _dirName); 28 | } 29 | } else { 30 | await MagpieFileUtils.writeFile( 31 | fileName: _fileName, contents: json, dirName: _dirName); 32 | print('$_tag writeStatisticsData data = $json'); 33 | } 34 | } 35 | } 36 | 37 | static Future readStatisticsData() async { 38 | return await MagpieFileUtils.readFile( 39 | fileName: _fileName, dirName: _dirName); 40 | } 41 | 42 | static Future isExistsStatistics() async { 43 | return await MagpieFileUtils.isExistsFile( 44 | fileName: _fileName, dirName: _dirName); 45 | } 46 | 47 | static Future clearStatisticsData() async { 48 | await MagpieFileUtils.rmFile(fileName: _fileName, dirName: _dirName); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/file/file_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:path_provider/path_provider.dart'; 5 | 6 | class MagpieFileUtils { 7 | static final String tag = 'Magpie FileUtils'; 8 | 9 | static final String baseFilePath = '/magpie'; 10 | 11 | ///根据指定文件夹名称创建文件 12 | ///[dirName]为空时,默认在magpie下创建文件 13 | static Future _createFile( 14 | {@required String fileName, String dirName}) async { 15 | Directory baseDir = await getApplicationDocumentsDirectory(); 16 | var filePath = Directory(baseDir.path); 17 | if (dirName.isNotEmpty) { 18 | filePath = Directory(baseDir.path + '/' + dirName); 19 | } 20 | 21 | try { 22 | bool isExsit = await filePath.exists(); 23 | print( 24 | '$tag " _createFile filePath = ${filePath.path} , isExsit = $isExsit'); 25 | if (!isExsit) { 26 | filePath.create(); 27 | } 28 | } catch (e) { 29 | print('$tag : _createFile error = $e'); 30 | } 31 | return File('${filePath.path}/$fileName'); 32 | } 33 | 34 | ///文件中写入数据 35 | static Future writeFile( 36 | {@required String fileName, 37 | @required String contents, 38 | String dirName}) async { 39 | if (fileName.isNotEmpty && contents.isNotEmpty) { 40 | File file = await _createFile(fileName: fileName, dirName: dirName); 41 | if (!(await file.exists())) { 42 | file.create(); 43 | } 44 | await (file).writeAsString(contents); 45 | print('$tag writeFile success : content = $contents'); 46 | } else { 47 | print('$tag writeFile error = fileName isEmpty or contents isEmpty'); 48 | } 49 | } 50 | 51 | ///获取文件。[dirName] 文件夹名称,[fileName] 文件名称 52 | static Future _getFile( 53 | {@required String fileName, String dirName}) async { 54 | if (fileName.isNotEmpty) { 55 | String filePath = dirName.isNotEmpty 56 | ? (await getApplicationDocumentsDirectory()).path + '/' + dirName 57 | : (await getApplicationDocumentsDirectory()).path; 58 | 59 | return File('$filePath/$fileName'); 60 | } else { 61 | print('$tag getFile error = fileName isEmpty'); 62 | return null; 63 | } 64 | } 65 | 66 | ///从文件中读书数据 67 | static Future readFile( 68 | {@required String fileName, String dirName}) async { 69 | try { 70 | File file = await _getFile(fileName: fileName, dirName: dirName); 71 | if (await file.exists()) { 72 | String contents = await file.readAsString(); 73 | 74 | return contents; 75 | } else { 76 | print('$tag readFile 确定创建了??? --- ${file.path}'); 77 | } 78 | } catch (e) { 79 | print('$tag readFile error = $e'); 80 | } 81 | 82 | return ''; 83 | } 84 | 85 | ///判断文件是否存在 86 | static Future isExistsFile( 87 | {@required String fileName, String dirName}) async { 88 | File file = await _getFile(fileName: fileName, dirName: dirName); 89 | if (await file.exists()) { 90 | print( 91 | '$tag isExistsFile = true, fileName = $fileName , dirName = $dirName'); 92 | return true; 93 | } else { 94 | print( 95 | '$tag isExistsFile = false, fileName = $fileName , dirName = $dirName'); 96 | return false; 97 | } 98 | } 99 | 100 | ///删除文件 101 | static Future rmFile( 102 | {@required String fileName, String dirName}) async { 103 | if (await isExistsFile(fileName: fileName, dirName: dirName)) { 104 | File file = await _getFile(fileName: fileName, dirName: dirName); 105 | file.delete(); 106 | 107 | print( 108 | '$tag rmFile rmove file, fileName = $fileName , dirName = $dirName'); 109 | } 110 | } 111 | 112 | ///清除文件中的数据 113 | static Future clearFileData( 114 | {@required String fileName, String dirName}) async { 115 | //暂且就先用简单直接的方式来吧。。。 116 | await rmFile(fileName: fileName, dirName: dirName); 117 | await _createFile(fileName: fileName, dirName: dirName); 118 | } 119 | 120 | ///获取文件路径 121 | static Future getFilePath( 122 | {@required String fileName, String dirName}) async { 123 | if (await isExistsFile(fileName: fileName, dirName: dirName)) { 124 | File file = await _getFile(fileName: fileName, dirName: dirName); 125 | return file.path; 126 | } else { 127 | return '文件路径不存在'; 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /lib/file/log_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:magpie_log/handler/statistics_handler.dart'; 3 | import 'package:magpie_log/model/analysis_model.dart'; 4 | import 'data_analysis.dart'; 5 | import 'dart:convert' as convert; 6 | 7 | class MagpieLogUtil { 8 | static void runTimeLogModel(AnalysisModel model) { 9 | runTimeLog(model.actionName, model.pagePath, 10 | convert.jsonDecode(model.analysisData)); 11 | } 12 | 13 | static void runTimeLog(String actionName, String pagePath, Map dataMap, 14 | {String type, int index = 0}) { 15 | MagpieDataAnalysis.readActionData( 16 | actionName: actionName, pagePath: pagePath, type: type) 17 | .then((data) { 18 | if (data == null) return; 19 | 20 | Map logMap = convert.jsonDecode(data.analysisData); 21 | trueData(dataMap, logMap, index: index); 22 | 23 | AnalysisModel model = AnalysisModel( 24 | actionName: actionName, 25 | pagePath: pagePath, 26 | type: type, 27 | analysisData: convert.jsonEncode(logMap), 28 | description: data.description); 29 | 30 | MagpieStatisticsHandler.instance.writeData(model.toJson()); 31 | debugPrint("runtime log:" + model.toJson().toString()); 32 | }); 33 | } 34 | 35 | static void trueData(Map dataMap, Map logMap, {int index = 0}) { 36 | logMap.forEach((k, v) { 37 | if (v is Map) { 38 | trueData(dataMap[k] is List ? dataMap[k][index] : dataMap[k], v, 39 | index: index); 40 | } else { 41 | logMap[k] = dataMap[k]; 42 | } 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/handler/statistics_handler.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | import 'package:device_info/device_info.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:magpie_log/file/data_analysis.dart'; 7 | import 'package:magpie_log/file/data_statistics.dart'; 8 | import 'package:magpie_log/file/file_utils.dart'; 9 | import 'package:magpie_log/model/analysis_model.dart'; 10 | 11 | import '../file/data_statistics.dart'; 12 | import '../magpie_log.dart'; 13 | import '../model/device_data.dart'; 14 | 15 | ///已圈选数据统计上报 16 | 17 | class MagpieStatisticsHandler { 18 | final String _tag = 'MagpieStatisticsHandler'; 19 | 20 | factory MagpieStatisticsHandler() => _getInstance(); 21 | 22 | static MagpieStatisticsHandler get instance => _getInstance(); 23 | 24 | static MagpieStatisticsHandler _instance; 25 | 26 | static MagpieStatisticsHandler _getInstance() { 27 | if (_instance == null) { 28 | _instance = MagpieStatisticsHandler._init(); 29 | } 30 | 31 | return _instance; 32 | } 33 | 34 | int _time, _count; 35 | 36 | ReportChannel _reportChannel; 37 | 38 | ReportMethod _reportMethod; 39 | 40 | get reportChannel => _reportChannel; 41 | 42 | get reportMethod => _reportMethod; 43 | 44 | void setReportChannel(ReportChannel channelType) { 45 | _reportChannel = channelType; 46 | print('ReportChannel value = $_reportChannel'); 47 | } 48 | 49 | void setReportMethod(ReportMethod method) { 50 | this._reportMethod = method; 51 | } 52 | 53 | Timer _timer; 54 | 55 | List> _dataStatistics; 56 | 57 | MagpieStatisticsHandler._init() { 58 | _dataStatistics = List(); 59 | } 60 | 61 | /** 62 | * 初始化配置。 63 | * [reportMethod] 数据上报方式,默认单条上报 64 | * [reportChannel] 数据上报通道,默认Flutter 65 | * [time] 定时上报方式需要设置的时间周期。默认为2*60*1000ms 66 | * [count] 计数上报方式需要设置的采集数量。默认为50条 67 | * [callback] 设置Flutter通信的callback。如果数据上报通过flutter实现,此方法必须实现!!! 68 | */ 69 | void initConfig(ReportMethod reportMethod, ReportChannel reportChannel, 70 | {int time: 2 * 60 * 1000, 71 | int count: 50, 72 | AnalysisCallback callback}) async { 73 | this._count = count; 74 | this._time = time; 75 | this._reportMethod = reportMethod; 76 | this._reportChannel = reportChannel; 77 | _MagpieAnalysisHandler.instance.initHandler(reportChannel, callback); 78 | print( 79 | '$_tag initConfig, reportMethod = $_reportMethod,reportChannel = $_reportChannel'); 80 | 81 | //返回公共参数。原则上初始化的时候需要上报一次,但是不强制 82 | String _deviceInfo = (await _createCommonParams()).toJson().toString(); 83 | 84 | ///初始化时判断是否有之前写入的未上报数据,有则上报后删除 85 | if (await MagpieDataStatistics.isExistsStatistics()) { 86 | await MagpieDataStatistics.writeStatisticsData( 87 | {'deviceInfo': _deviceInfo}); 88 | 89 | _MagpieAnalysisHandler.instance 90 | .sendDataToJson(await MagpieDataStatistics.readStatisticsData()); 91 | MagpieDataStatistics.clearStatisticsData(); 92 | } else { 93 | _MagpieAnalysisHandler.instance 94 | .sendDataToMap({'deviceInfo': _deviceInfo}); 95 | } 96 | } 97 | 98 | ///写入要上报的圈选数据 99 | void writeData(Map data) { 100 | if (_reportMethod != ReportMethod.each) { 101 | _saveData(data); 102 | } 103 | if (_reportMethod == ReportMethod.timing) { 104 | if (_timer == null) { 105 | _reportDataToTimer(); 106 | } 107 | } else if (_reportMethod == ReportMethod.total) { 108 | _reportDataToCount(data); 109 | } else { 110 | _MagpieAnalysisHandler.instance.sendDataToMap(data); 111 | } 112 | } 113 | 114 | /// app退出时调用 115 | void exitMagpie() { 116 | if (_timer.isActive) { 117 | _timer.cancel(); 118 | _timer = null; 119 | } 120 | } 121 | 122 | void _saveData(Map data) async { 123 | if (data != null && data.isNotEmpty) { 124 | _dataStatistics.add(data); 125 | } 126 | if (await MagpieDataStatistics.isExistsStatistics()) { 127 | MagpieDataStatistics.writeStatisticsData(data); 128 | } else { 129 | MagpieDataStatistics.writeStatisticsData({'data': _dataStatistics}); 130 | } 131 | } 132 | 133 | //定时上报 134 | void _reportDataToTimer() { 135 | _timer = Timer.periodic(Duration(milliseconds: _time), (method) async { 136 | if (_dataStatistics.isNotEmpty || 137 | await MagpieDataStatistics.isExistsStatistics()) { 138 | var data; 139 | if (_dataStatistics.isEmpty) { 140 | data = await MagpieDataStatistics.readStatisticsData(); 141 | } else { 142 | var params = {'data': _dataStatistics}; 143 | data = jsonEncode(params).toString(); 144 | } 145 | _sendData(data); 146 | } 147 | }); 148 | } 149 | 150 | //计数上报 151 | void _reportDataToCount(Map data) async { 152 | if (_dataStatistics.length >= _count) { 153 | var params = {'data': _dataStatistics}; 154 | String jsonData = jsonEncode(params).toString(); 155 | 156 | _sendData(jsonData); 157 | } 158 | } 159 | 160 | void _sendData(String jsonData) async { 161 | _MagpieAnalysisHandler.instance.sendDataToJson(jsonData); 162 | _dataStatistics.clear(); 163 | await MagpieDataStatistics.clearStatisticsData(); 164 | } 165 | 166 | ///构造公共参数 167 | Future _createCommonParams() async { 168 | DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); 169 | var platform, deviceVersion, clientId, deviceName, deviceId, model; 170 | if (Platform.isAndroid) { 171 | AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; 172 | platform = 'Android'; 173 | deviceVersion = androidInfo.version.release; 174 | deviceName = androidInfo.brand; 175 | model = androidInfo.model; 176 | deviceId = androidInfo.androidId; 177 | } else if (Platform.isIOS) { 178 | IosDeviceInfo iosInfo = await deviceInfo.iosInfo; 179 | platform = "iOS"; 180 | deviceVersion = iosInfo.systemVersion; 181 | deviceName = iosInfo.name; 182 | model = iosInfo.model; 183 | deviceId = iosInfo.identifierForVendor; 184 | } 185 | clientId = globalClientId; 186 | 187 | DeviceData info = DeviceData( 188 | platform, clientId, deviceName, deviceId, deviceVersion, model); 189 | 190 | print('$_tag createCommonParams Android device Info = ${info.toJson()}'); 191 | 192 | return info; 193 | } 194 | } 195 | 196 | ///非单条上报时,每次数据add到list中时,一并写入到文件中 197 | ///数据上报优先以文件数据为准 198 | ///上报后清空内存缓存和文件 199 | 200 | typedef AnalysisCallback = Function(String jsonData); 201 | 202 | class _MagpieAnalysisHandler { 203 | static final String _tag = 'AnalysisHandler'; 204 | 205 | static final String _channelName = 'magpie_analysis_channel'; 206 | 207 | //上报圈选数据通道类型,0 - Flutter,1 - Native 208 | ReportChannel _channelType; 209 | 210 | factory _MagpieAnalysisHandler() => _getInstance(); 211 | 212 | static _MagpieAnalysisHandler get instance => _getInstance(); 213 | 214 | static _MagpieAnalysisHandler _instance; 215 | 216 | var _msgChannel; 217 | 218 | AnalysisCallback _callback; 219 | 220 | //发送圈选数据 221 | void sendDataToMap(Map data) { 222 | if (data == null || data.isEmpty) { 223 | print('$_tag sendData data 不合法!!!'); 224 | return; 225 | } 226 | 227 | sendDataToJson(jsonEncode(data).toString()); 228 | } 229 | 230 | void sendDataToJson(String jsonData) { 231 | if (jsonData.isNotEmpty) { 232 | if (_channelType == ReportChannel.natives) { 233 | _MagpieAnalysisHandler.instance._sendMsgToNative(jsonData); 234 | } else { 235 | _MagpieAnalysisHandler.instance._sendMsgToFlutter(jsonData); 236 | } 237 | print('$_tag sendDataToJson jsonData = $jsonData'); 238 | } 239 | } 240 | 241 | //设置Flutter通信的callback。如果数据上报通过flutter实现,此方法必须实现!!! 242 | void initHandler(ReportChannel channelType, AnalysisCallback callback) { 243 | this._channelType = channelType; 244 | if (channelType != ReportChannel.natives) { 245 | this._callback = callback; 246 | } 247 | } 248 | 249 | _MagpieAnalysisHandler._handler() { 250 | _msgChannel = BasicMessageChannel(_channelName, StringCodec()); 251 | } 252 | 253 | static _MagpieAnalysisHandler _getInstance() { 254 | if (_instance == null) { 255 | _instance = _MagpieAnalysisHandler._handler(); 256 | } 257 | return _instance; 258 | } 259 | 260 | Future _sendMagpieData(String data) async { 261 | await _msgChannel.send(data); 262 | } 263 | 264 | //通过callback发送数据给Flutter 265 | void _sendMsgToFlutter(String data) { 266 | if (_callback == null) { 267 | print('$_tag callback is null'); 268 | return; 269 | } 270 | _callback(data); 271 | } 272 | 273 | //通过BasicMessageChannel发送数据给Native 274 | void _sendMsgToNative(String data) { 275 | _sendMagpieData(data); 276 | print('$_tag sendMsgToNative : ${jsonEncode(data).toString()}'); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /lib/interceptor/intercepter_screen_log.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_redux/flutter_redux.dart'; 3 | import 'package:magpie_log/file/log_util.dart'; 4 | import 'package:magpie_log/magpie_constants.dart'; 5 | import 'package:magpie_log/magpie_log.dart'; 6 | import 'package:magpie_log/ui/log_float_view.dart'; 7 | import 'package:magpie_log/ui/log_screen.dart'; 8 | 9 | /// 1.LogObserver是页面跳转监听,拦截页面入栈用于统计页面曝光 10 | /// 11 | /// 2.MagpieLog.instance.routeStack路由栈的维护,用于获取页面的唯一标识 12 | /// 和action拦截的页面弹出,action拦截需要route用于弹层 13 | /// 14 | class LogObserver extends NavigatorObserver { 15 | @override 16 | void didPush(Route route, Route previousRoute) async { 17 | //当前route添加到路由堆栈 18 | MagpieLog.instance.routeStack.add(route); 19 | 20 | //排除 21 | if (route.settings.name.startsWith(MagpieConstants.magpieDomain)) { 22 | return; 23 | } 24 | await Future.delayed(Duration(milliseconds: 500)); //可以去掉 25 | try { 26 | LogState logState = 27 | StoreProvider.of(route.navigator.context).state as LogState; 28 | var json = logState.toJson(); 29 | 30 | String actionName = 31 | MagpieLog.instance.getCurrentPath(); //TODO:考虑actionName可以自定义 32 | String pagePath = MagpieLog.instance.getCurrentPath(); 33 | 34 | if (MagpieLog.instance.isDebug && MagpieLog.instance.isPageLogOn) { 35 | FloatEntry.singleton.showOverlayEntry(); 36 | Future.delayed(Duration.zero, () { 37 | try { 38 | MagpieLog.instance.addToActionList( 39 | actionName, 40 | PageRouteBuilder( 41 | opaque: false, 42 | settings: RouteSettings(name: MagpieConstants.logScreen), 43 | pageBuilder: (context, animation, secondaryAnimation) => 44 | LogScreen( 45 | pagePath: pagePath, 46 | actionName: actionName, 47 | data: logState.toJson(), 48 | logType: pageType))); 49 | } catch (e) { 50 | print(e.toString()); 51 | } 52 | }); 53 | } else { 54 | MagpieLogUtil.runTimeLog(actionName, pagePath, json, type: pageType); 55 | } 56 | } catch (e) { 57 | debugPrint(e.toString()); 58 | } 59 | } 60 | 61 | @override 62 | void didPop(Route route, Route previousRoute) { 63 | MagpieLog.instance.routeStack.removeLast(); 64 | } 65 | 66 | @override 67 | void didRemove(Route route, Route previousRoute) { 68 | MagpieLog.instance.routeStack.removeLast(); 69 | } 70 | 71 | @override 72 | void didReplace({Route newRoute, Route oldRoute}) { 73 | MagpieLog.instance.routeStack.removeLast(); 74 | MagpieLog.instance.routeStack.add(newRoute); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/interceptor/interceptor_circle_log.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_redux/flutter_redux.dart'; 3 | import 'package:magpie_log/file/log_util.dart'; 4 | import 'package:magpie_log/magpie_constants.dart'; 5 | import 'package:magpie_log/magpie_log.dart'; 6 | import 'package:magpie_log/ui/log_screen.dart'; 7 | import 'package:redux/redux.dart'; 8 | 9 | ///step:1 intercept to add circle log 10 | 11 | class CircleMiddleWare extends MiddlewareClass { 12 | @override 13 | void call(Store store, action, NextDispatcher next) { 14 | LogState logState = store.state; 15 | Map json = logState.toJson(); 16 | print("MyMiddleWare call:${json.toString()}"); 17 | String actionName = 18 | action is LogAction ? action.actionName : action.toString().replaceAll( 19 | "Instance of ", "").replaceAll("'", ""); 20 | String pagePath = MagpieLog.instance.getCurrentPath(); 21 | if (MagpieLog.instance.isDebug) { 22 | try { 23 | MagpieLog.instance.addToActionList( 24 | actionName + "|" + pagePath, 25 | PageRouteBuilder( 26 | opaque: false, 27 | settings: RouteSettings(name: MagpieConstants.logScreen), 28 | pageBuilder: (context, animation, secondaryAnimation) => 29 | LogScreen( 30 | data: json, 31 | logType: reduxType, 32 | store: store, 33 | action: action, 34 | pagePath: pagePath, 35 | actionName: actionName, 36 | next: next))); 37 | } catch (e) { 38 | print(e.toString()); 39 | } 40 | } else { 41 | MagpieLogUtil.runTimeLog(actionName, pagePath, json, 42 | type: reduxType, index: action is LogAction ? action.index : 0); 43 | } 44 | next(action); 45 | } 46 | } 47 | 48 | class LogStoreConnector extends StoreConnector { 49 | final ViewModelBuilder builder; 50 | final StoreConverter converter; 51 | final bool distinct; 52 | final OnInitCallback onInit; 53 | final OnDisposeCallback onDispose; 54 | final bool rebuildOnChange; 55 | final IgnoreChangeTest ignoreChange; 56 | final OnWillChangeCallback onWillChange; 57 | final OnDidChangeCallback onDidChange; 58 | final OnInitialBuildCallback onInitialBuild; 59 | 60 | const LogStoreConnector({ 61 | Key key, 62 | @required this.builder, 63 | @required this.converter, 64 | this.distinct = false, 65 | this.onInit, 66 | this.onDispose, 67 | this.rebuildOnChange = true, 68 | this.ignoreChange, 69 | this.onWillChange, 70 | this.onDidChange, 71 | this.onInitialBuild, 72 | }) : super(key: key, builder: builder, converter: converter); 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | LogState logState = StoreProvider 77 | .of(context) 78 | .state as LogState; 79 | var width = 0.0; 80 | if (logState.logStatus == 1) { 81 | width = 2.0; 82 | } 83 | return Container( 84 | decoration: BoxDecoration( 85 | border: Border.all(color: Colors.red, width: width), 86 | ), 87 | child: super.build(context)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/interceptor/interceptor_state_log.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:magpie_log/file/log_util.dart'; 4 | import 'package:magpie_log/magpie_constants.dart'; 5 | import 'package:magpie_log/magpie_log.dart'; 6 | import 'package:magpie_log/ui/log_screen.dart'; 7 | 8 | abstract class WidgetLogState extends State { 9 | String getActionName(); 10 | 11 | int getIndex(); 12 | 13 | var logStatus; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | if (logStatus == 1 && MagpieLog.instance.isDebug) 18 | return Container( 19 | decoration: BoxDecoration( 20 | border: Border.all(color: Colors.red, width: 2.0), 21 | ), 22 | child: onBuild(context)); 23 | else 24 | return onBuild(context); 25 | } 26 | 27 | Widget onBuild(BuildContext context); 28 | 29 | @override 30 | void setState(VoidCallback fn) { 31 | Map json = toJson(); 32 | print("MyMiddleWare call:${json.toString()}"); 33 | var actionName = getActionName(); 34 | String pagePath = MagpieLog.instance.getCurrentPath(); 35 | if (MagpieLog.instance.isDebug) { 36 | MagpieLog.instance.getCurrentRoute().navigator.push(PageRouteBuilder( 37 | opaque: false, 38 | settings: RouteSettings(name: MagpieConstants.logScreen), 39 | pageBuilder: (context, animation, secondaryAnimation) => LogScreen( 40 | data: json, 41 | logType: stateType, 42 | pagePath: pagePath, 43 | actionName: actionName, 44 | func: fn, 45 | state: this, 46 | ))); 47 | } else { 48 | MagpieLogUtil.runTimeLog(actionName, pagePath, json, 49 | type: stateType, index: getIndex()); 50 | super.setState(fn); 51 | } 52 | } 53 | 54 | setRealState(Function func) { 55 | super.setState(func); 56 | } 57 | 58 | @override 59 | void initState() { 60 | super.initState(); 61 | } 62 | 63 | Map toJson(); 64 | } 65 | -------------------------------------------------------------------------------- /lib/magpie_constants.dart: -------------------------------------------------------------------------------- 1 | class MagpieConstants { 2 | static final String magpieDomain = '/magpie_log'; 3 | 4 | //圈选日志页面 5 | static final String logScreen = magpieDomain + '/log_screen'; 6 | 7 | //圈选数据操作 8 | static final String operationScreen = magpieDomain + '/log_operation'; 9 | 10 | //圈选数据列表页面 11 | static final String actionScreen = magpieDomain + '/log_action_list'; 12 | 13 | //选择数据上报方式页面 14 | static final String selectChannelScreen = 15 | magpieDomain + '/log_select_channel_screen'; 16 | } 17 | 18 | //页面t 19 | const String pageType = 'page'; 20 | //state统计 21 | const String stateType = 'state'; 22 | //redux统计 23 | const String reduxType = 'redux'; 24 | -------------------------------------------------------------------------------- /lib/magpie_log.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:magpie_log/ui/log_float_view.dart'; 5 | 6 | import 'file/data_analysis.dart'; 7 | import 'handler/statistics_handler.dart'; 8 | import 'model/analysis_model.dart'; 9 | 10 | //const bool isDebug = const bool.fromEnvironment("dart.vm.product"); 11 | const bool globalIsDebug = !const bool.fromEnvironment("dart.vm.product"); 12 | const bool globalIsPageLogOn = true; 13 | const String globalClientId = "com.wuba.flutter.magpie_log"; 14 | 15 | typedef PageNameCallback = String Function(Route route); 16 | 17 | class MagpieLog { 18 | // 工厂模式 19 | factory MagpieLog() => _getInstance(); 20 | 21 | static MagpieLog get instance => _getInstance(); 22 | static MagpieLog _instance; 23 | 24 | MagpieLog._internal() { 25 | // 初始化 26 | } 27 | 28 | bool isDebug = globalIsDebug; 29 | bool isPageLogOn = globalIsPageLogOn; 30 | List> routeStack = List(); 31 | 32 | PageNameCallback pageNameCallback; 33 | 34 | static bool _isInit = false; 35 | 36 | static MagpieLog _getInstance() { 37 | if (_instance == null) { 38 | _instance = new MagpieLog._internal(); 39 | } 40 | return _instance; 41 | } 42 | 43 | init(BuildContext context, ReportMethod reportMethod, 44 | ReportChannel reportChannel, 45 | {int time, 46 | int count, 47 | AnalysisCallback callback, 48 | PageNameCallback pageNameCallback}) { 49 | // 暂时还没有更好的办法来处理某些只需要初始化一次的函数 50 | 51 | this.pageNameCallback = pageNameCallback; 52 | 53 | if (!_isInit) { 54 | MagpieDataAnalysis.initMagpieData(context); //初始化圈选数据 55 | MagpieStatisticsHandler.instance.initConfig(reportMethod, reportChannel, 56 | callback: callback, 57 | time: time != null ? time : 1 * 60 * 1000, 58 | count: count != null ? count : 3); 59 | _isInit = true; 60 | } 61 | } 62 | 63 | String getCurrentPath() { 64 | if (pageNameCallback != null) { 65 | return pageNameCallback(getCurrentRoute()); 66 | } 67 | 68 | return getCurrentRoute() != null 69 | ? getCurrentRoute().settings.name 70 | : "not define"; 71 | } 72 | 73 | BuildContext getCurrentContext() { 74 | Route route = getCurrentRoute(); 75 | return route.navigator.context; 76 | } 77 | 78 | Route getCurrentRoute() { 79 | return MagpieLog.instance.routeStack.length > 0 80 | ? routeStack[MagpieLog.instance.routeStack.length - 1] 81 | : null; 82 | } 83 | 84 | ///actionListMap 85 | ///事件列表 86 | LinkedHashMap actionListMap = LinkedHashMap(); 87 | 88 | addToActionList(String key, Route route) { 89 | actionListMap[key] = route; 90 | FloatEntry.singleton.refresh(); 91 | } 92 | 93 | removeFromActionList(String key) { 94 | actionListMap.remove(key); 95 | } 96 | } 97 | 98 | abstract class LogState { 99 | int get logStatus => _logStatus; 100 | int _logStatus; 101 | 102 | Map toJson(); 103 | } 104 | 105 | class LogAction { 106 | static LogAction setUp(actionName, index, actionParams) { 107 | return LogAction(actionName, index: index, actionParams: actionParams); 108 | } 109 | 110 | LogAction(this.actionName, {this.index = 0, this.actionParams}); 111 | 112 | String actionName; 113 | int index; //用于列表页position 114 | Map actionParams; //action拓展参数 115 | } 116 | -------------------------------------------------------------------------------- /lib/model/analysis_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | import 'package:magpie_log/handler/statistics_handler.dart'; 4 | 5 | part 'analysis_model.g.dart'; 6 | 7 | @JsonSerializable() 8 | class AnalysisModel { 9 | //事件名称 10 | final String actionName; 11 | 12 | //页面路径 13 | final String pagePath; 14 | 15 | //埋点数据 16 | String analysisData; 17 | 18 | //圈选数据描述 19 | String description; 20 | 21 | //圈选数据类型 22 | String type; 23 | 24 | AnalysisModel( 25 | {@required this.actionName, 26 | @required this.pagePath, 27 | @required this.analysisData, 28 | @required this.description, 29 | this.type}); 30 | 31 | factory AnalysisModel.fromJson(Map json) => 32 | _$AnalysisModelFromJson(json); 33 | 34 | Map toJson() => _$AnalysisModelToJson(this); 35 | 36 | set updateAnalysis(String data) { 37 | this.analysisData = data; 38 | } 39 | 40 | @override 41 | String toString() { 42 | return 'actionName : $actionName , pageName : $pagePath , analysisData : $analysisData , description : $description , type : $type'; 43 | } 44 | } 45 | 46 | @JsonSerializable() 47 | class AnalysisData { 48 | List data; 49 | 50 | //数据上报通道 51 | ReportChannel reportChannel; 52 | 53 | //数据上报方式 54 | ReportMethod reportMethod; 55 | 56 | AnalysisData(this.data, this.reportChannel, this.reportMethod) { 57 | MagpieStatisticsHandler.instance.setReportChannel(reportChannel); 58 | MagpieStatisticsHandler.instance.setReportMethod(reportMethod); 59 | } 60 | 61 | factory AnalysisData.fromJson(Map json) => 62 | _$AnalysisDataFromJson(json); 63 | 64 | Map toJson() => _$AnalysisDataToJson(this); 65 | } 66 | 67 | /// 数据上报方式 68 | enum ReportMethod { 69 | each, //每条上报 70 | timing, //定时上报 71 | total, //计数上报 72 | } 73 | 74 | ///数据上报通道 75 | enum ReportChannel { 76 | flutter, //Flutter 通道 77 | natives, //Native BasicMessageChannel 通道 78 | } 79 | 80 | 81 | -------------------------------------------------------------------------------- /lib/model/analysis_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'analysis_model.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | AnalysisModel _$AnalysisModelFromJson(Map json) { 10 | return AnalysisModel( 11 | actionName: json['actionName'] as String, 12 | pagePath: json['pagePath'] as String, 13 | analysisData: json['analysisData'] as String, 14 | description: json['description'] as String, 15 | type: json['type'] as String, 16 | ); 17 | } 18 | 19 | Map _$AnalysisModelToJson(AnalysisModel instance) => 20 | { 21 | 'actionName': instance.actionName, 22 | 'pagePath': instance.pagePath, 23 | 'analysisData': instance.analysisData, 24 | 'description': instance.description, 25 | 'type': instance.type, 26 | }; 27 | 28 | AnalysisData _$AnalysisDataFromJson(Map json) { 29 | return AnalysisData( 30 | (json['data'] as List) 31 | ?.map((e) => e == null 32 | ? null 33 | : AnalysisModel.fromJson(e as Map)) 34 | ?.toList(), 35 | decodeChannel(json['reportChannel']), 36 | decodeMethod(json['reportMethod']), 37 | ); 38 | } 39 | 40 | Map _$AnalysisDataToJson(AnalysisData instance) => 41 | { 42 | 'data': instance.data, 43 | 'reportChannel': endoceChannel(instance.reportChannel), 44 | 'reportMethod': encodeMethod(instance.reportMethod), 45 | }; 46 | 47 | int encodeMethod(ReportMethod method) { 48 | if (method == ReportMethod.timing) { 49 | return 1; 50 | } else if (method == ReportMethod.total) { 51 | return 2; 52 | } else { 53 | return 0; 54 | } 55 | } 56 | 57 | ReportMethod decodeMethod(dynamic method) { 58 | if (method == 1) { 59 | return ReportMethod.timing; 60 | } else if (method == 2) { 61 | return ReportMethod.total; 62 | } else { 63 | return ReportMethod.each; 64 | } 65 | } 66 | 67 | int endoceChannel(ReportChannel channel) { 68 | if (channel == ReportChannel.natives) { 69 | return 1; 70 | } else { 71 | return 0; 72 | } 73 | } 74 | 75 | ReportChannel decodeChannel(dynamic channel) { 76 | if (channel == 1) { 77 | return ReportChannel.natives; 78 | } else { 79 | return ReportChannel.flutter; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/model/device_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | part 'device_data.g.dart'; 3 | 4 | ///采集设备信息和安装包信息 5 | @JsonSerializable() 6 | class DeviceData { 7 | final String platform; 8 | 9 | final String clientId; 10 | 11 | final String deviceVersion; 12 | 13 | final String brand; 14 | 15 | final String deviceId; 16 | 17 | final String model; 18 | 19 | DeviceData(this.clientId, this.platform, this.brand, this.deviceId, 20 | this.deviceVersion, this.model); 21 | 22 | factory DeviceData.fromJson(Map params) => 23 | _$DeviceDataFromJson(params); 24 | 25 | Map toJson() => _$DeviceDataToJson(this); 26 | } 27 | -------------------------------------------------------------------------------- /lib/model/device_data.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'device_data.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | DeviceData _$DeviceDataFromJson(Map json) { 10 | return DeviceData( 11 | json['clientId'], 12 | json['platform'], 13 | json['brand'], 14 | json['deviceId'], 15 | json['deviceVersion'], 16 | json['model'], 17 | ); 18 | } 19 | 20 | Map _$DeviceDataToJson(DeviceData instance) => { 21 | 'platform': instance.platform, 22 | 'clientId': instance.clientId, 23 | 'deviceVersion': instance.deviceVersion, 24 | 'brand': instance.brand, 25 | 'deviceId': instance.deviceId, 26 | 'model': instance.model, 27 | }; 28 | -------------------------------------------------------------------------------- /lib/ui/log_actiion_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:magpie_log/file/data_analysis.dart'; 3 | 4 | ///查看已圈选数据列表 5 | class MagpieActionList extends StatefulWidget { 6 | @override 7 | State createState() => _ActionListState(); 8 | } 9 | 10 | class _ActionListState extends State { 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | title: Text('圈选配置列表(侧滑删除)'), 16 | centerTitle: true, 17 | leading: Builder(builder: (BuildContext context) { 18 | return IconButton( 19 | icon: Icon(Icons.arrow_back_ios), 20 | onPressed: () { 21 | Navigator.pop(context); 22 | }); 23 | }), 24 | ), 25 | body: Container( 26 | color: Color(0xFFf6f7fb), 27 | child: _getListView(), 28 | )); 29 | } 30 | 31 | ListView _getListView() => ListView.separated( 32 | itemCount: MagpieDataAnalysis.getListData().length, 33 | separatorBuilder: (context, index) { 34 | return Divider( 35 | height: 0, 36 | ); 37 | }, 38 | itemBuilder: (BuildContext context, int position) { 39 | return _getItem(position); 40 | }, 41 | ); 42 | 43 | Container _getItem(int position) => Container( 44 | color: Colors.white, 45 | margin: EdgeInsets.fromLTRB(0, 0, 0, 10), 46 | child: Dismissible( 47 | onDismissed: (_) { 48 | MagpieDataAnalysis.getListData().removeAt(position); 49 | }, 50 | direction: DismissDirection.endToStart, 51 | background: Container( 52 | color: Colors.deepOrange, 53 | child: Align( 54 | alignment: Alignment.centerRight, 55 | child: Container( 56 | child: Text( 57 | '删除', 58 | style: TextStyle( 59 | color: Colors.white, 60 | fontWeight: FontWeight.bold, 61 | fontSize: 14, 62 | ), 63 | textAlign: TextAlign.center, 64 | ), 65 | margin: EdgeInsets.fromLTRB(0, 0, 50, 0), 66 | ))), 67 | key: Key(MagpieDataAnalysis.getListData()[position].actionName), 68 | child: Padding( 69 | padding: EdgeInsets.fromLTRB(15, 15, 15, 15), 70 | child: Column( 71 | crossAxisAlignment: CrossAxisAlignment.start, 72 | children: [ 73 | Text( 74 | '标识: ${MagpieDataAnalysis.getListData()[position].actionName}', 75 | textAlign: TextAlign.left, 76 | style: TextStyle( 77 | color: Colors.black54, 78 | fontWeight: FontWeight.bold, 79 | fontSize: 14), 80 | ), 81 | Text( 82 | '路由: ${MagpieDataAnalysis.getListData()[position].pagePath}', 83 | textAlign: TextAlign.left, 84 | style: TextStyle(color: Colors.black54, fontSize: 14), 85 | ), 86 | Text( 87 | '描述: ${MagpieDataAnalysis.getListData()[position].description}', 88 | textAlign: TextAlign.left, 89 | style: TextStyle(color: Colors.black54, fontSize: 14), 90 | ), 91 | Container( 92 | margin: EdgeInsets.fromLTRB(0, 5, 0, 0), 93 | child: Text( 94 | '参数: ${MagpieDataAnalysis.getListData()[position].analysisData}', 95 | textAlign: TextAlign.left, 96 | style: TextStyle(color: Colors.black54, fontSize: 14), 97 | ), 98 | ), 99 | ], 100 | )), 101 | )); 102 | } 103 | -------------------------------------------------------------------------------- /lib/ui/log_float_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:magpie_log/magpie_log.dart'; 3 | 4 | import '../magpie_constants.dart'; 5 | import 'log_operation_screen.dart'; 6 | 7 | ///时间列表坦弹层页面 8 | class FloatEntry { 9 | static final FloatEntry _instance = FloatEntry(); 10 | 11 | static FloatEntry get singleton => _instance; 12 | OverlayEntry _entry; 13 | GlobalKey<_DraggableBtnState> childKey = GlobalKey(); 14 | 15 | showOverlayEntry() { 16 | try { 17 | _entry?.remove(); 18 | } catch (e) { 19 | debugPrint(e.toString()); 20 | } 21 | if (_entry == null) { 22 | _entry = new OverlayEntry(builder: (context) { 23 | return DraggableBtn( 24 | key: childKey, 25 | child: getChild(), 26 | initOffset: Offset(MediaQuery.of(context).size.width - 200, 300), 27 | ); 28 | }); 29 | } 30 | MagpieLog.instance.getCurrentRoute().navigator.overlay.insert(_entry); 31 | } 32 | 33 | ///弹层的核心组件 34 | Widget getChild() { 35 | return Container( 36 | width: 200, 37 | decoration: BoxDecoration(boxShadow: [ 38 | BoxShadow( 39 | color: Color(0xAA9BBBBBB), 40 | blurRadius: 5.0, 41 | offset: Offset(1.0, 1.0), 42 | ), 43 | ], borderRadius: BorderRadius.circular(3), color: Color(0xAAFFFFFF)), 44 | alignment: Alignment.center, 45 | child: Column( 46 | mainAxisAlignment: MainAxisAlignment.spaceAround, 47 | crossAxisAlignment: CrossAxisAlignment.center, 48 | children: _getListView())); 49 | } 50 | 51 | ///弹层的列表组件开发 52 | List _getListView() { 53 | List listWidget = []; 54 | listWidget.add(Container( 55 | height: 30, 56 | padding: EdgeInsets.fromLTRB(10, 0, 10, 0), 57 | decoration: BoxDecoration( 58 | color: Color(0x99ff552e), 59 | borderRadius: BorderRadius.vertical(top: Radius.circular(3))), 60 | child: Row( 61 | children: [ 62 | Expanded( 63 | child: Text("事件列表", 64 | style: TextStyle( 65 | fontSize: 14, 66 | color: Colors.white, 67 | decoration: TextDecoration.none, 68 | ))), 69 | GestureDetector( 70 | child: Container( 71 | padding: EdgeInsets.only(right: 10), 72 | child: Text( 73 | "配置", 74 | style: TextStyle( 75 | fontSize: 14, 76 | color: Colors.white, 77 | decoration: TextDecoration.none, 78 | ), 79 | ), 80 | ), 81 | onTap: () { 82 | MagpieLog.instance.getCurrentRoute().navigator.push( 83 | MaterialPageRoute( 84 | settings: 85 | RouteSettings(name: MagpieConstants.operationScreen), 86 | builder: (BuildContext context) { 87 | return MagpieLogOperation(); 88 | })); 89 | }, 90 | ), 91 | GestureDetector( 92 | child: Icon(Icons.close, color: Colors.white), 93 | onTap: () { 94 | _entry?.remove(); 95 | }, 96 | ) 97 | ], 98 | ), 99 | )); 100 | 101 | for (String key in MagpieLog.instance.actionListMap.keys) { 102 | listWidget.add(GestureDetector( 103 | behavior: HitTestBehavior.opaque, 104 | child: Container( 105 | width: 200, 106 | padding: EdgeInsets.all(10), 107 | child: Center( 108 | child: Text( 109 | key, 110 | style: TextStyle( 111 | fontSize: 14, 112 | color: Colors.black45, 113 | decoration: TextDecoration.none, 114 | ), 115 | ), 116 | )), 117 | onTap: () { 118 | MagpieLog.instance 119 | .getCurrentRoute() 120 | .navigator 121 | .push(MagpieLog.instance.actionListMap[key]); 122 | 123 | MagpieLog.instance.removeFromActionList(key); 124 | refresh(); 125 | }, 126 | )); 127 | } 128 | 129 | return listWidget; 130 | } 131 | 132 | void refresh() { 133 | // ignore: invalid_use_of_protected_member 134 | childKey.currentState.setState(() { 135 | childKey.currentState.child = getChild(); 136 | }); 137 | } 138 | } 139 | 140 | ///拖拽控件 141 | class DraggableBtn extends StatefulWidget { 142 | final Widget child; 143 | final Offset initOffset; 144 | final Offset fullScreenOffset; 145 | 146 | const DraggableBtn({ 147 | Key key, 148 | @required this.child, 149 | this.initOffset, 150 | this.fullScreenOffset = Offset.zero, 151 | }) : super(key: key); 152 | 153 | @override 154 | _DraggableBtnState createState() => _DraggableBtnState(child); 155 | } 156 | 157 | class _DraggableBtnState extends State 158 | with SingleTickerProviderStateMixin { 159 | var _animController; 160 | var _scaleAnim; 161 | var _left = 50.0; 162 | var _top = 50.0; 163 | var globalKey = GlobalKey(); 164 | var childWidth; 165 | var childHeight; 166 | Widget child; 167 | 168 | _DraggableBtnState(this.child); 169 | 170 | @override 171 | void initState() { 172 | _left = widget.initOffset.dx; 173 | _top = widget.initOffset.dy; 174 | WidgetsBinding.instance.addPostFrameCallback((_) { 175 | RenderBox renderBox = globalKey.currentContext.findRenderObject(); 176 | childWidth = renderBox.size.width; 177 | childHeight = renderBox.size.height; 178 | }); 179 | _animController = 180 | AnimationController(vsync: this, duration: Duration(milliseconds: 200)); 181 | _scaleAnim = Tween(begin: 1.0, end: 1.0).animate(_animController); 182 | super.initState(); 183 | } 184 | 185 | @override 186 | void dispose() { 187 | _animController.dispose(); 188 | super.dispose(); 189 | } 190 | 191 | @override 192 | Widget build(BuildContext context) { 193 | return Stack( 194 | children: [ 195 | Positioned( 196 | left: _left, 197 | top: _top, 198 | child: Draggable( 199 | childWhenDragging: Container(), 200 | feedback: ScaleTransition(scale: _scaleAnim, child: child), 201 | child: Container( 202 | key: globalKey, 203 | child: child, 204 | ), 205 | onDragStarted: () { 206 | _animController.forward(); 207 | }, 208 | onDragCompleted: () {}, 209 | onDraggableCanceled: (v, offset) { 210 | debugPrint("offset y ${offset.dy}"); 211 | var size = MediaQuery.of(context).size; 212 | setState(() { 213 | _left = offset.dx - widget.fullScreenOffset.dx; 214 | _top = offset.dy - widget.fullScreenOffset.dy; 215 | if (_left < 0) _left = 0; 216 | if (_left > size.width - childWidth) { 217 | _left = size.width - childWidth; 218 | } 219 | if (_top < 0) _top = 0; 220 | if (_top > size.height - childHeight) { 221 | _top = size.height - childHeight; 222 | } 223 | }); 224 | }, 225 | ), 226 | ) 227 | ], 228 | ); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /lib/ui/log_operation_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/rendering.dart'; 3 | import 'package:fluttertoast/fluttertoast.dart'; 4 | import 'package:magpie_log/file/data_analysis.dart'; 5 | import 'package:magpie_log/magpie_constants.dart'; 6 | import 'package:magpie_log/magpie_log.dart'; 7 | import 'package:magpie_log/ui/log_select_report.dart'; 8 | 9 | import 'log_actiion_list.dart'; 10 | 11 | /// 已圈选数据操作页面 12 | class MagpieLogOperation extends StatefulWidget { 13 | @override 14 | _LogOperationState createState() => _LogOperationState(); 15 | } 16 | 17 | class _LogOperationState extends State { 18 | String checkLog, delLog, savePath; 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Scaffold( 28 | appBar: AppBar( 29 | centerTitle: true, 30 | title: Text('圈选配置'), 31 | leading: Builder(builder: (BuildContext context) { 32 | return IconButton( 33 | icon: Icon(Icons.arrow_back_ios), 34 | onPressed: () { 35 | Navigator.pop(context); 36 | }, 37 | ); 38 | }), 39 | ), 40 | body: Container( 41 | color: Color(0xFFf6f7fb), 42 | child: ListView( 43 | children: [ 44 | listItem("查看圈选数据", 45 | leftWidget: Icon( 46 | Icons.list, 47 | color: Colors.black26, 48 | size: 15, 49 | ), onTap: () { 50 | Navigator.of(context).push(MaterialPageRoute( 51 | settings: RouteSettings(name: MagpieConstants.actionScreen), 52 | builder: (BuildContext context) { 53 | return MagpieActionList(); 54 | })); 55 | }), 56 | listItem("保存圈选数据至文件", 57 | leftWidget: Icon( 58 | Icons.save_alt, 59 | color: Colors.black26, 60 | size: 15, 61 | ), onTap: () { 62 | MagpieDataAnalysis.saveData().then((data) async { 63 | MagpieDataAnalysis.getSavePath().then((path) { 64 | Fluttertoast.showToast( 65 | msg: '数据已保存至:$path', toastLength: Toast.LENGTH_SHORT); 66 | }); 67 | }); 68 | }), 69 | listItem("清除全部圈选数据", 70 | leftWidget: Icon( 71 | Icons.delete_forever, 72 | color: Colors.black26, 73 | size: 15, 74 | ), onTap: () { 75 | MagpieDataAnalysis.clearAnalysisData().then((value) { 76 | Fluttertoast.showToast( 77 | msg: '数据清除成功~', toastLength: Toast.LENGTH_SHORT); 78 | }); 79 | }), 80 | listItem("选择数据上报方式", 81 | marginBottom: 10.0, 82 | leftWidget: Icon( 83 | Icons.cloud_upload, 84 | color: Colors.black26, 85 | size: 15, 86 | ), onTap: () { 87 | Navigator.of(context).push(MaterialPageRoute( 88 | settings: RouteSettings( 89 | name: MagpieConstants.selectChannelScreen), 90 | builder: (BuildContext context) { 91 | return MagpieSelectReport(); 92 | })); 93 | }), 94 | listItem("打开Debug模式", 95 | content: "是否打开圈选 关闭上传埋点 \n需重启才能开启", 96 | leftWidget: Icon( 97 | Icons.adjust, 98 | color: Colors.black26, 99 | size: 15, 100 | ), 101 | rightWidget: Switch( 102 | value: MagpieLog.instance.isDebug, 103 | onChanged: (value) { 104 | setState(() { 105 | MagpieLog.instance.isDebug = 106 | !MagpieLog.instance.isDebug; 107 | }); 108 | }, 109 | activeTrackColor: Colors.deepOrange, 110 | activeColor: Colors.white, 111 | )), 112 | listItem("打开页面圈选", 113 | content: "是否打开页面展示圈选 \n开启跳转0.5秒后打开圈选页面", 114 | leftWidget: Icon( 115 | Icons.content_copy, 116 | color: Colors.black26, 117 | size: 15, 118 | ), 119 | rightWidget: Switch( 120 | value: MagpieLog.instance.isPageLogOn, 121 | onChanged: (value) { 122 | setState(() { 123 | MagpieLog.instance.isPageLogOn = 124 | !MagpieLog.instance.isPageLogOn; 125 | }); 126 | }, 127 | activeTrackColor: Colors.deepOrange, 128 | activeColor: Colors.white, 129 | )), 130 | ], 131 | )), 132 | ); 133 | } 134 | 135 | Widget listItem(String title, 136 | {Widget leftWidget, 137 | String content, 138 | Widget rightWidget, 139 | GestureTapCallback onTap, 140 | marginBottom}) { 141 | return GestureDetector( 142 | onTap: onTap, 143 | child: Container( 144 | height: 50, 145 | color: Colors.white, 146 | margin: EdgeInsets.fromLTRB( 147 | 0, 0, 0, marginBottom != null ? marginBottom : 0.5), 148 | padding: EdgeInsets.fromLTRB(15, 0, 10, 0), 149 | child: Row( 150 | children: [ 151 | leftWidget != null 152 | ? leftWidget 153 | : Icon( 154 | Icons.settings, 155 | color: Colors.black38, 156 | size: 15, 157 | ), 158 | Container(width: 5), 159 | Expanded( 160 | child: Column( 161 | mainAxisAlignment: MainAxisAlignment.center, 162 | crossAxisAlignment: CrossAxisAlignment.stretch, 163 | children: [ 164 | Container( 165 | child: Text( 166 | title, 167 | style: TextStyle(fontSize: 14), 168 | ), 169 | ) 170 | ])), 171 | rightWidget != null 172 | ? rightWidget 173 | : Icon( 174 | Icons.chevron_right, 175 | color: Colors.black26, 176 | ), 177 | ], 178 | ))); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /lib/ui/log_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert' as convert; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:fluttertoast/fluttertoast.dart'; 6 | import 'package:magpie_log/file/data_analysis.dart'; 7 | import 'package:magpie_log/interceptor/interceptor_state_log.dart'; 8 | import 'package:magpie_log/magpie_constants.dart'; 9 | import 'package:magpie_log/magpie_log.dart'; 10 | import 'package:magpie_log/model/analysis_model.dart'; 11 | import 'package:redux/redux.dart'; 12 | import 'package:rubber/rubber.dart'; 13 | 14 | import 'log_operation_screen.dart'; 15 | 16 | class LogScreen extends StatefulWidget { 17 | //通用参数 18 | final String logType; //埋点类型 19 | final Map data; 20 | final String actionName; 21 | final String pagePath; 22 | 23 | //circleLogType参数 24 | final dynamic action; //没有actionName用action替代 25 | final Store store; 26 | final NextDispatcher next; 27 | 28 | //stateLogType参数 29 | final Function func; 30 | final WidgetLogState state; 31 | 32 | const LogScreen({Key key, 33 | @required this.data, 34 | @required this.logType, 35 | @required this.actionName, 36 | this.store, 37 | this.pagePath, 38 | this.action, 39 | this.next, 40 | this.func, 41 | this.state}) 42 | : super(key: key); 43 | 44 | @override 45 | _LogScreenState createState() => _LogScreenState(); 46 | } 47 | 48 | class ParamItem { 49 | String key; 50 | String value; 51 | bool isChecked; //是否选中 52 | bool isPaneled; //是否展开 53 | List paramItems; //子参数item 54 | 55 | ParamItem(this.key, this.value, 56 | {this.paramItems, this.isPaneled = false, this.isChecked = false}); 57 | } 58 | 59 | class _LogScreenState extends State 60 | with SingleTickerProviderStateMixin { 61 | //抽屉滚动监听 62 | RubberAnimationController _controller; 63 | ScrollController _scrollController = ScrollController(); 64 | 65 | String title = ""; //标题展示 66 | String description = ""; //描述展示 67 | String type = ""; //埋点类型展示 68 | bool isExpanded = false; //抽屉是否打开 69 | bool isModify = true; //是否是修改状态 70 | 71 | List paramList = []; //参数列表数据 72 | 73 | @override 74 | void initState() { 75 | //埋点类型判断 76 | switch (widget.logType) { 77 | case pageType: 78 | title = "页面圈选"; 79 | break; 80 | case reduxType: 81 | title = "点击圈选-Redux"; 82 | break; 83 | case stateType: 84 | title = "点击圈选-State"; 85 | break; 86 | } 87 | type = "${widget.logType}($title)"; 88 | //初始化抽屉组件 89 | _controller = RubberAnimationController( 90 | vsync: this, 91 | halfBoundValue: AnimationControllerValue(percentage: 0.9), 92 | lowerBoundValue: AnimationControllerValue(pixel: 205), 93 | duration: Duration(milliseconds: 200)); 94 | _controller.animationState.addListener(_stateListener); 95 | 96 | //初始化已有埋点数据 97 | MagpieDataAnalysis.readActionData( 98 | actionName: widget.actionName, pagePath: widget.pagePath) 99 | .then((logModel) { 100 | Map map; 101 | if (null != logModel) { 102 | if (logModel.analysisData != null && logModel.analysisData != "") { 103 | map = convert.jsonDecode(logModel.analysisData); 104 | } 105 | description = logModel.description; 106 | isModify = false; 107 | } 108 | initParam(widget.data, map, paramList); 109 | setState(() {}); 110 | }); 111 | 112 | super.initState(); 113 | } 114 | 115 | ///递归参数数据初始化 116 | bool initParam(Map data, Map logConfig, List paramList) { 117 | if (data == null) return false; 118 | 119 | bool isPaneled = false; //是不是展开 120 | bool isParentPaneled = false; //父View是不是展开 121 | data.forEach((k, v) { 122 | List paramList2 = []; 123 | bool isChecked = false; 124 | if (v is Map) { 125 | isPaneled = 126 | initParam(v, logConfig == null ? null : logConfig[k], paramList2); 127 | } else if (v is List && v != null && v.length > 0) { 128 | isPaneled = initParam( 129 | v[0] is Map ? v[0] : null, 130 | logConfig == null || logConfig[k] == null ? null : logConfig[k], 131 | paramList2); 132 | } else { 133 | if (logConfig != null && logConfig[k] != null && logConfig[k] == 1) { 134 | isChecked = true; 135 | isParentPaneled = true; 136 | } 137 | } 138 | paramList.add(ParamItem(k, v.toString(), 139 | paramItems: paramList2, isChecked: isChecked, isPaneled: isPaneled)); 140 | }); 141 | //自己是展开的或者父View是展开的都要返回true 142 | return isPaneled || isParentPaneled; 143 | } 144 | 145 | ///抽屉状态监听 改变按钮样式和展开收起事件 146 | void _stateListener() { 147 | switch (_controller.animationState.value) { 148 | case AnimationState.expanded: 149 | case AnimationState.half_expanded: 150 | setState(() { 151 | isExpanded = true; 152 | }); 153 | break; 154 | case AnimationState.collapsed: 155 | setState(() { 156 | isExpanded = false; 157 | }); 158 | break; 159 | case AnimationState.animating: 160 | break; 161 | } 162 | } 163 | 164 | @override 165 | Widget build(BuildContext context) { 166 | return Scaffold( 167 | backgroundColor: Color(0x66000000), 168 | body: Column(children: [ 169 | Expanded( 170 | child: RubberBottomSheet( 171 | header: _getHeader(), 172 | headerHeight: 50, 173 | scrollController: _scrollController, 174 | lowerLayer: _getLowerLayer(), 175 | upperLayer: _getUpperLayer(), 176 | animationController: _controller, 177 | )), 178 | Container(height: 0.5, color: Color(0xFFf6f7fb)), 179 | buttons(), 180 | ])); 181 | } 182 | 183 | ///抽屉头部组件 184 | Widget _getHeader() { 185 | return Container( 186 | child: Stack(children: [ 187 | Container( 188 | color: Colors.white, 189 | padding: EdgeInsets.fromLTRB(15, 0, 10, 0), 190 | margin: EdgeInsets.fromLTRB(0, 0, 0, 0), 191 | child: Row(children: [ 192 | GestureDetector( 193 | onTap: () { 194 | Navigator.pop(context); 195 | }, 196 | child: Icon( 197 | Icons.arrow_back_ios, 198 | color: Colors.black, 199 | )), 200 | Container(width: 50), 201 | Expanded( 202 | child: Container(), 203 | ), 204 | GestureDetector( 205 | onTap: () { 206 | Navigator.of(context).push(MaterialPageRoute( 207 | settings: 208 | RouteSettings(name: MagpieConstants.operationScreen), 209 | builder: (BuildContext context) { 210 | return MagpieLogOperation(); 211 | })); 212 | }, 213 | child: Padding( 214 | padding: EdgeInsets.fromLTRB(0, 0, 15, 0), 215 | child: Text('圈选配置', 216 | style: TextStyle( 217 | color: Colors.black54, 218 | fontSize: 16, 219 | )), 220 | )), 221 | GestureDetector( 222 | onTap: () { 223 | isExpanded ? _controller.collapse() : _controller 224 | .expand(); 225 | }, 226 | child: Row(children: [ 227 | Text(isExpanded ? "收起" : "展开", 228 | style: TextStyle( 229 | color: Colors.black54, 230 | fontSize: 16, 231 | )), 232 | Icon( 233 | isExpanded ? Icons.arrow_drop_down : Icons 234 | .arrow_drop_up, 235 | color: Colors.black, 236 | ) 237 | ])), 238 | ])), 239 | ])); 240 | } 241 | 242 | ///抽屉背景 243 | Widget _getLowerLayer() { 244 | return GestureDetector( 245 | onTap: () { 246 | //Navigator.pop(context); 247 | }, 248 | child: Container( 249 | decoration: BoxDecoration(color: Colors.transparent), 250 | )); 251 | } 252 | 253 | ///抽屉上层组件 254 | Widget _getUpperLayer() { 255 | return Container( 256 | color: Colors.white, 257 | child: MediaQuery.removePadding( 258 | removeTop: true, 259 | context: context, 260 | child: ListView( 261 | controller: _scrollController, 262 | children: initPanelList(paramList), 263 | ))); 264 | } 265 | 266 | ///底部按钮 267 | Widget buttons() { 268 | return Container( 269 | color: Colors.white, 270 | child: Row( 271 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 272 | children: [ 273 | getButtonItem( 274 | "跳过", MediaQuery 275 | .of(context) 276 | .size 277 | .width / 4, Colors.white, () { 278 | switch (widget.logType) { 279 | case pageType: 280 | break; 281 | case stateType: 282 | widget.state.setRealState(widget.func); 283 | break; 284 | case reduxType: 285 | widget.next(widget.action); 286 | break; 287 | } 288 | Navigator.pop(context); 289 | }, textColor: Colors.deepOrange), 290 | getButtonItem( 291 | "保存", MediaQuery 292 | .of(context) 293 | .size 294 | .width / 4, Colors.white, () { 295 | MagpieDataAnalysis.saveData().then((data) async { 296 | MagpieDataAnalysis.getSavePath().then((path) { 297 | Fluttertoast.showToast( 298 | msg: '数据已保存至:$path', toastLength: Toast.LENGTH_SHORT); 299 | }); 300 | }); 301 | }, textColor: Colors.deepOrange), 302 | getButtonItem( 303 | isModify ? "埋点" : "修改", 304 | MediaQuery 305 | .of(context) 306 | .size 307 | .width / 2, 308 | isModify ? Colors.deepOrange : Colors.deepOrange, () { 309 | if (isModify) { 310 | if (widget.logType == stateType) { 311 | widget.state.logStatus = 1; 312 | widget.state.setRealState(() {}); 313 | } 314 | 315 | Map map = Map(); 316 | getLog(map, paramList); 317 | String log = convert.jsonEncode(map); 318 | 319 | String type = widget.logType; 320 | 321 | MagpieDataAnalysis.writeData(AnalysisModel( 322 | actionName: widget.actionName, 323 | pagePath: widget.pagePath, 324 | analysisData: log, 325 | description: description, 326 | type: type)) 327 | .then((value) { 328 | Fluttertoast.showToast(msg: "埋点添加成功\n请及时保存!"); 329 | 330 | setState(() { 331 | isModify = !isModify; 332 | }); 333 | }); 334 | } else { 335 | setState(() { 336 | isModify = !isModify; 337 | }); 338 | } 339 | }), 340 | ], 341 | )); 342 | } 343 | 344 | ///底部按钮Button封装 345 | Widget getButtonItem(text, width, color, onPressed, {textColor}) { 346 | return Container( 347 | height: 45, 348 | width: width, 349 | child: FlatButton( 350 | shape: RoundedRectangleBorder( 351 | side: BorderSide.none, borderRadius: BorderRadius.zero), 352 | color: color, 353 | child: Text(text, 354 | style: TextStyle( 355 | fontSize: 16, 356 | color: textColor != null ? textColor : Colors.white)), 357 | onPressed: onPressed, 358 | )); 359 | } 360 | 361 | ///主体列表全组件 362 | List initPanelList(List paramList) { 363 | List list = []; 364 | list.add(getBasicTitle("基础配置")); 365 | list.add(getBasicItem("事件标识: ", widget.actionName)); 366 | list.add(getBasicItem("页面路径: ", widget.pagePath)); 367 | list.add(getBasicItem("埋点类型: ", type)); 368 | //描述信息TextField 369 | list.add( 370 | Padding( 371 | padding: EdgeInsets.fromLTRB(15, 0, 15, 10), 372 | child: Row( 373 | children: [ 374 | Text( 375 | "描述信息: ", 376 | style: TextStyle( 377 | fontSize: 14, 378 | color: Color(0xFF999999), 379 | ), 380 | ), 381 | SizedBox( 382 | width: 200, 383 | child: TextField( 384 | enabled: isModify, 385 | controller: TextEditingController(text: description), 386 | onChanged: (text) { 387 | description = text; 388 | }, 389 | style: TextStyle( 390 | fontSize: 14, 391 | color: Colors.black, 392 | ), 393 | decoration: InputDecoration( 394 | hintStyle: 395 | TextStyle(fontSize: 14, color: Color(0xFF999999)), 396 | hintText: '如:列表页点击'), 397 | )), 398 | ], 399 | )), 400 | ); 401 | if (isModify) { 402 | list.add(getBasicTitle("参数配置")); 403 | } else { 404 | list.add(getBasicTitle("埋点数据")); 405 | } 406 | //添加所有参数view 407 | list.addAll(intChildList(paramList)); 408 | 409 | return list; 410 | } 411 | 412 | Widget getBasicTitle(title) { 413 | return Container( 414 | padding: EdgeInsets.fromLTRB(15, 5, 5, 5), 415 | width: MediaQuery 416 | .of(context) 417 | .size 418 | .width, 419 | color: Color(0xFFF6F7FB), 420 | child: Text( 421 | title, 422 | style: TextStyle(fontSize: 14, color: Color(0xFF999999)), 423 | ), 424 | ); 425 | } 426 | 427 | Widget getBasicItem(key, value) { 428 | return Padding( 429 | padding: EdgeInsets.fromLTRB(15, 15, 15, 0), 430 | child: Row( 431 | children: [ 432 | Text( 433 | key, 434 | style: TextStyle( 435 | fontSize: 14, 436 | color: Color(0xFF999999), 437 | ), 438 | ), 439 | Text( 440 | value ?? "", 441 | style: TextStyle( 442 | fontSize: 14, 443 | color: Colors.black, 444 | ), 445 | ), 446 | ], 447 | )); 448 | } 449 | 450 | ///递归参数圈选列表 451 | List intChildList(List paramList) { 452 | List widgetList = []; 453 | 454 | for (int index = 0; index < paramList.length; index++) { 455 | if (paramList[index].paramItems.length == 0) { 456 | widgetList.add(getFinalItem(paramList[index])); 457 | } else { 458 | widgetList.add(Column( 459 | crossAxisAlignment: CrossAxisAlignment.stretch, 460 | children: [ 461 | GestureDetector( 462 | onTap: () { 463 | setState(() { 464 | paramList[index].isPaneled = !paramList[index].isPaneled; 465 | }); 466 | }, 467 | child: Padding( 468 | padding: EdgeInsets.fromLTRB(15, 10, 0, 0), 469 | child: Row(children: [ 470 | !paramList[index].isPaneled 471 | ? Icon(Icons.add, size: 15, color: Colors.black54) 472 | : Icon(Icons.remove, size: 15, color: Colors.black54), 473 | Padding( 474 | padding: EdgeInsets.fromLTRB(10, 0, 0, 0), 475 | child: Text(paramList[index].key, 476 | style: TextStyle( 477 | color: Colors.black54, 478 | fontSize: 13, 479 | ))) 480 | ]), 481 | )), 482 | getPaneledItem(paramList[index].isPaneled, paramList[index]) 483 | ])); 484 | } 485 | } 486 | 487 | return widgetList; 488 | } 489 | 490 | Widget getPaneledItem(bool isPaneled, ParamItem paramItem) { 491 | if (isPaneled) { 492 | return Padding( 493 | padding: EdgeInsets.fromLTRB(15, 0, 0, 0), 494 | child: Column( 495 | crossAxisAlignment: CrossAxisAlignment.start, 496 | children: intChildList(paramItem.paramItems))); 497 | } else { 498 | return Container(); 499 | } 500 | } 501 | 502 | Widget getFinalItem(ParamItem paramItem) { 503 | if (!isModify) { 504 | if (paramItem.isChecked) { 505 | return Container(height: 40, child: getCheckBoxTile(paramItem)); 506 | } else { 507 | return Container(); 508 | } 509 | } else { 510 | return Container(height: 40, child: getCheckBoxTile(paramItem)); 511 | } 512 | } 513 | 514 | Widget getCheckBoxTile(ParamItem paramItem) { 515 | return Row(children: [ 516 | Padding( 517 | padding: EdgeInsets.fromLTRB(40, 0, 0, 0), 518 | child: Text(paramItem.key, 519 | style: TextStyle( 520 | color: isModify ? Colors.black54 : Colors.orange, 521 | fontSize: 13))), 522 | Expanded( 523 | child: Text(": " + paramItem.value, 524 | style: TextStyle(color: Colors.black26, fontSize: 13)), 525 | ), 526 | getCheckBox(paramItem) 527 | ]); 528 | } 529 | 530 | Widget getCheckBox(ParamItem paramItem) { 531 | return isModify 532 | ? Checkbox( 533 | value: paramItem.isChecked, 534 | onChanged: (bool) { 535 | setState(() { 536 | paramItem.isChecked = bool; 537 | }); 538 | }, 539 | ) 540 | : Container(); 541 | } 542 | 543 | ///生成日志递归 544 | bool getLog(Map map, List paramList) { 545 | bool isChecked = false; 546 | paramList.forEach((param) { 547 | if (param.paramItems.length == 0 && param.isChecked == true) { 548 | map[param.key] = 1; 549 | isChecked = true; 550 | } else { 551 | Map childMap = Map(); 552 | if (getLog(childMap, param.paramItems)) { 553 | map[param.key] = childMap; 554 | isChecked = true; 555 | } 556 | } 557 | }); 558 | return isChecked; 559 | } 560 | } 561 | -------------------------------------------------------------------------------- /lib/ui/log_select_report.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:magpie_log/handler/statistics_handler.dart'; 3 | import 'package:magpie_log/model/analysis_model.dart'; 4 | 5 | ///选择数据上报方式 6 | 7 | class MagpieSelectReport extends StatefulWidget { 8 | @override 9 | State createState() { 10 | return _SelectReport(); 11 | } 12 | } 13 | 14 | class _SelectReport extends State { 15 | TextEditingController _totalCon = TextEditingController(), 16 | _minCon = TextEditingController(), 17 | _secsCon = TextEditingController(); 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Scaffold( 27 | appBar: AppBar( 28 | centerTitle: true, 29 | title: Text('修改数据上报方式'), 30 | leading: Builder( 31 | builder: (BuildContext context) { 32 | return IconButton( 33 | icon: Icon(Icons.arrow_back_ios), 34 | onPressed: () { 35 | Navigator.pop(context); 36 | }); 37 | }, 38 | ), 39 | ), 40 | body: SingleChildScrollView( 41 | child: Container( 42 | color: Color(0xFFf6f7fb), 43 | child: Column( 44 | crossAxisAlignment: CrossAxisAlignment.start, 45 | mainAxisAlignment: MainAxisAlignment.start, 46 | children: [ 47 | Container( 48 | width: MediaQuery.of(context).size.width, 49 | color: Colors.white, 50 | padding: EdgeInsets.fromLTRB(15, 25, 15, 25), 51 | child: Text( 52 | '修改后请记得及时保存,否则它只有在内存中存活的机会', 53 | textAlign: TextAlign.start, 54 | style: TextStyle( 55 | color: Colors.deepOrange, 56 | fontSize: 14, 57 | ), 58 | )), 59 | Container( 60 | padding: EdgeInsets.fromLTRB(15, 20, 15, 7), 61 | child: Text( 62 | '选择数据上报通道:', 63 | textAlign: TextAlign.left, 64 | style: TextStyle(color: Colors.black, fontSize: 14), 65 | ), 66 | ), 67 | Container( 68 | padding: EdgeInsets.fromLTRB(15, 0, 15, 0), 69 | color: Colors.white, 70 | child: Column( 71 | crossAxisAlignment: CrossAxisAlignment.start, 72 | mainAxisAlignment: MainAxisAlignment.start, 73 | children: [ 74 | RadioListTile( 75 | value: 0, 76 | groupValue: _channelType, 77 | activeColor: Colors.deepOrange, 78 | title: Text( 79 | 'Flutter channel', 80 | textAlign: TextAlign.left, 81 | style: TextStyle( 82 | fontSize: 14, 83 | ), 84 | ), 85 | subtitle: Text( 86 | '通过Flutter端上报圈选统计数据', 87 | textAlign: TextAlign.left, 88 | style: TextStyle(fontSize: 12), 89 | ), 90 | controlAffinity: ListTileControlAffinity.leading, 91 | selected: _channelType == 0, 92 | onChanged: (value) { 93 | _changeChannel(value); 94 | }, 95 | ), 96 | Container( 97 | color: Color(0xFFf6f7fb), 98 | height: 1, 99 | width: MediaQuery.of(context).size.width), 100 | RadioListTile( 101 | value: 1, 102 | groupValue: _channelType, 103 | activeColor: Colors.deepOrange, 104 | title: Text( 105 | 'Native channel', 106 | textAlign: TextAlign.left, 107 | style: TextStyle( 108 | fontSize: 14, 109 | ), 110 | ), 111 | subtitle: Text( 112 | '通过Native message Channel上报圈选统计数据', 113 | textAlign: TextAlign.left, 114 | style: TextStyle(fontSize: 12), 115 | ), 116 | controlAffinity: ListTileControlAffinity.leading, 117 | selected: _channelType == 1, 118 | onChanged: (value) { 119 | _changeChannel(value); 120 | }, 121 | ), 122 | ], 123 | )), 124 | Container( 125 | padding: EdgeInsets.fromLTRB(15, 20, 15, 7), 126 | child: Text( 127 | '选择数据上报方式:', 128 | textAlign: TextAlign.left, 129 | style: TextStyle(color: Colors.black, fontSize: 14), 130 | ), 131 | ), 132 | Container( 133 | padding: EdgeInsets.fromLTRB(15, 0, 15, 0), 134 | color: Colors.white, 135 | child: Column( 136 | mainAxisAlignment: MainAxisAlignment.start, 137 | children: [ 138 | RadioListTile( 139 | value: 0, 140 | groupValue: _methodType, 141 | activeColor: Colors.deepOrange, 142 | title: Text( 143 | '每条上报', 144 | textAlign: TextAlign.left, 145 | style: TextStyle( 146 | fontSize: 14, 147 | ), 148 | ), 149 | subtitle: Text( 150 | '每次圈选数据触发后立刻上报', 151 | textAlign: TextAlign.left, 152 | style: TextStyle(fontSize: 12), 153 | ), 154 | controlAffinity: ListTileControlAffinity.leading, 155 | selected: _methodType == 0, 156 | onChanged: (value) { 157 | _changeMethod(value); 158 | }, 159 | ), 160 | Container( 161 | color: Color(0xFFf6f7fb), 162 | height: 1, 163 | width: MediaQuery.of(context).size.width), 164 | RadioListTile( 165 | value: 1, 166 | groupValue: _methodType, 167 | activeColor: Colors.deepOrange, 168 | title: Text( 169 | '定时上报', 170 | textAlign: TextAlign.left, 171 | style: TextStyle( 172 | fontSize: 14, 173 | ), 174 | ), 175 | subtitle: Text( 176 | '设置定时周期,定时上报周期内统计的圈选数据。默认定时周期:2 * 60 * 1000ms', 177 | textAlign: TextAlign.left, 178 | style: TextStyle(fontSize: 12)), 179 | controlAffinity: ListTileControlAffinity.leading, 180 | selected: _methodType == 1, 181 | onChanged: (value) { 182 | _changeMethod(value); 183 | }, 184 | ), 185 | Container( 186 | color: Color(0xFFf6f7fb), 187 | height: 1, 188 | width: MediaQuery.of(context).size.width), 189 | RadioListTile( 190 | value: 2, 191 | groupValue: _methodType, 192 | activeColor: Colors.deepOrange, 193 | title: Text( 194 | '计数上报', 195 | textAlign: TextAlign.left, 196 | style: TextStyle( 197 | fontSize: 14, 198 | ), 199 | ), 200 | subtitle: Text('设置统计数量阈值,达到阈值后自动上报圈选数据', 201 | textAlign: TextAlign.left, 202 | style: TextStyle(fontSize: 12)), 203 | controlAffinity: ListTileControlAffinity.leading, 204 | selected: _methodType == 2, 205 | onChanged: (value) { 206 | _changeMethod(value); 207 | }, 208 | ), 209 | // _buildTotalWidget(), 210 | ], 211 | ), 212 | ) 213 | ], 214 | )), 215 | )); 216 | } 217 | 218 | Widget _buildTimeWidget() { 219 | if (_methodType == 1) { 220 | return Container( 221 | margin: EdgeInsets.fromLTRB(30, 5, 20, 10), 222 | child: Row( 223 | mainAxisAlignment: MainAxisAlignment.start, 224 | children: [ 225 | Text( 226 | '设置间隔时间:', 227 | textAlign: TextAlign.center, 228 | style: TextStyle(fontSize: 12, color: Colors.black), 229 | ), 230 | Container( 231 | width: 40, 232 | child: TextField( 233 | maxLines: 1, 234 | textAlign: TextAlign.center, 235 | keyboardType: TextInputType.number, 236 | decoration: InputDecoration(helperText: 'min'), 237 | controller: _minCon, 238 | style: TextStyle(fontSize: 12, color: Colors.black, height: 1), 239 | ), 240 | ), 241 | Text( 242 | ' * ', 243 | textAlign: TextAlign.center, 244 | style: TextStyle(fontSize: 12, color: Colors.black), 245 | ), 246 | Container( 247 | width: 40, 248 | child: TextField( 249 | maxLines: 1, 250 | textAlign: TextAlign.center, 251 | keyboardType: TextInputType.number, 252 | style: TextStyle(fontSize: 12, color: Colors.black, height: 1), 253 | decoration: 254 | InputDecoration(labelText: '1 - 60', helperText: 'secs'), 255 | controller: _secsCon, 256 | ), 257 | ), 258 | Text( 259 | ' * 1000ms', 260 | textAlign: TextAlign.center, 261 | style: TextStyle(fontSize: 12, color: Colors.black), 262 | ), 263 | ], 264 | ), 265 | ); 266 | } else { 267 | return Container(); 268 | } 269 | } 270 | 271 | Widget _buildTotalWidget() { 272 | if (_methodType == 2) { 273 | return Container( 274 | margin: EdgeInsets.fromLTRB(30, 5, 20, 10), 275 | child: Row( 276 | mainAxisAlignment: MainAxisAlignment.start, 277 | children: [ 278 | Text( 279 | '设置数据达到:', 280 | textAlign: TextAlign.center, 281 | style: TextStyle(fontSize: 12, color: Colors.black), 282 | ), 283 | Container( 284 | width: 40, 285 | child: TextField( 286 | maxLines: 1, 287 | textAlign: TextAlign.center, 288 | keyboardType: TextInputType.number, 289 | style: TextStyle(fontSize: 12, color: Colors.black, height: 1), 290 | controller: _totalCon, 291 | ), 292 | ), 293 | Text( 294 | ' 条时上报', 295 | textAlign: TextAlign.center, 296 | style: TextStyle(fontSize: 12, color: Colors.black), 297 | ) 298 | ], 299 | ), 300 | ); 301 | } else { 302 | return Container(); 303 | } 304 | } 305 | 306 | int _channelType = 307 | MagpieStatisticsHandler.instance.reportChannel == ReportChannel.natives 308 | ? 1 309 | : 0; 310 | int _methodType = _getReportMethod(); 311 | 312 | void _changeChannel(int value) { 313 | setState(() { 314 | _channelType = value; 315 | }); 316 | 317 | if (value == 1) { 318 | MagpieStatisticsHandler.instance.setReportChannel(ReportChannel.natives); 319 | } else { 320 | MagpieStatisticsHandler.instance.setReportChannel(ReportChannel.flutter); 321 | } 322 | } 323 | 324 | void _changeMethod(int value) { 325 | setState(() { 326 | _methodType = value; 327 | }); 328 | 329 | if (value == 1) { 330 | MagpieStatisticsHandler.instance.setReportMethod(ReportMethod.timing); 331 | } else if (value == 2) { 332 | MagpieStatisticsHandler.instance.setReportMethod(ReportMethod.total); 333 | } else { 334 | MagpieStatisticsHandler.instance.setReportMethod(ReportMethod.each); 335 | } 336 | } 337 | 338 | static int _getReportMethod() { 339 | if (MagpieStatisticsHandler.instance.reportMethod == ReportMethod.timing) { 340 | return 1; 341 | } else if (MagpieStatisticsHandler.instance.reportMethod == 342 | ReportMethod.total) { 343 | return 2; 344 | } else { 345 | return 0; 346 | } 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /local.properties: -------------------------------------------------------------------------------- 1 | ## This file must *NOT* be checked into Version Control Systems, 2 | # as it contains information specific to your local configuration. 3 | # 4 | # Location of the SDK. This is only used by Gradle. 5 | # For customization when using a Version Control System, please read the 6 | # header note. 7 | #Fri Dec 13 15:08:48 CST 2019 8 | sdk.dir=/Users/a58/Library/Android/sdk 9 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: magpie_log 2 | description: add log to flutter project with UI 3 | version: 1.0.1 4 | homepage: "" 5 | 6 | # The following defines the version and build number for your application. 7 | # A version number is three numbers separated by dots, like 1.2.43 8 | # followed by an optional build number separated by a +. 9 | # Both the version and the builder number may be overridden in flutter 10 | # build by specifying --build-name and --build-number, respectively. 11 | # In Android, build-name is used as versionName while build-number used as versionCode. 12 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 13 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 14 | # Read more about iOS versioning at 15 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 16 | 17 | environment: 18 | sdk: ">=2.2.2 <3.0.0" 19 | 20 | dependencies: 21 | flutter: 22 | sdk: flutter 23 | #redux 24 | redux: ^3.0.0 25 | flutter_redux: ^0.5.2 26 | #文件操作 27 | path_provider: ^1.5.0 28 | #设备信息 29 | device_info: ^0.4.1+3 30 | #Json 31 | json_annotation: ^3.0.0 32 | #toast 33 | fluttertoast: ^3.1.3 34 | 35 | # The following adds the Cupertino Icons font to your application. 36 | # Use with the CupertinoIcons class for iOS style icons. 37 | cupertino_icons: ^0.1.2 38 | #抽屉组件 39 | rubber: ^0.4.0 40 | 41 | dev_dependencies: 42 | flutter_test: 43 | sdk: flutter 44 | build_runner: ^1.7.0 45 | json_serializable: ^3.2.3 46 | # For information on the generic Dart part of this file, see the 47 | # following page: https://dart.dev/tools/pub/pubspec 48 | 49 | # The following section is specific to Flutter. 50 | flutter: 51 | plugin: 52 | androidPackage: io.flutter.plugins.wuba.magpielog 53 | pluginClass: MagpieLogPlugin 54 | # The following line ensures that the Material Icons font is 55 | # included with your application, so that you can use the icons in 56 | # the material Icons class. 57 | uses-material-design: true 58 | # To add assets to your application, add an assets section, like this: 59 | assets: 60 | - images/uncheck_icon.png 61 | - images/check_icon.png 62 | # An image asset can refer to one or more resolution-specific "variants", see 63 | # https://flutter.dev/assets-and-images/#resolution-aware. 64 | # For details regarding adding assets from package dependencies, see 65 | # https://flutter.dev/assets-and-images/#from-packages 66 | # To add custom fonts to your application, add a fonts section here, 67 | # in this "flutter" section. Each entry in this list should have a 68 | # "family" key with the font family name, and a "fonts" key with a 69 | # list giving the asset and other descriptors for the font. For 70 | # example: 71 | # fonts: 72 | # - family: Schyler 73 | # fonts: 74 | # - asset: fonts/Schyler-Regular.ttf 75 | # - asset: fonts/Schyler-Italic.ttf 76 | # style: italic 77 | # - family: Trajan Pro 78 | # fonts: 79 | # - asset: fonts/TrajanPro.ttf 80 | # - asset: fonts/TrajanPro_Bold.ttf 81 | # weight: 700 82 | # 83 | # For details regarding fonts from package dependencies, 84 | # see https://flutter.dev/custom-fonts/#from-packages 85 | --------------------------------------------------------------------------------