Flutter 项目开发

Flutter 中文网
Flutter 文档
Dart 文档
Pub 插件包
Github Flutter

路由

可以使用 Fluro

这个路由库的传参方式好像只有一种,不太灵活;传个中文的参数,必须编码,否则报错;路由的切换动画使用方便。

框架的路由功能:命名路由不能传参;路由栈的实例化方式可以传中文,路由退栈时也可以传数据。


// 框架的路由功能,还有很多... 
Navigator.push(context, ...).then((params){...});
Navigator.pop(context, params);

dependencies:
fluro: ^1.4.0

// 常用的方法
final router = Router();

var usersHandler = Handler(handlerFunc: (BuildContext context, Map params) {
  return UsersScreen(params["id"][0]);
});

void defineRoutes(Router router) {
  router.define("users/:id", handler: usersHandler, transitionType: TransitionType.inFromLeft);
}

router.navigateTo(context, "/users/1234", transition: TransitionType.fadeIn);

// 具体项目的用法
// application.dart 文件
class Application {
  static Router router;
}

// route_handlers.dart 文件
var demoRouteHandler = new Handler(
    handlerFunc: (BuildContext context, Map> params) {
  		String message = params["message"]?.first;
  		
  		return new DemoSimpleComponent(message: message);
});

// routes.dart 文件
class Routes {
  static String root = "/";
  static String demoSimple = "/demo";

  static void configureRoutes(Router router) {
    router.notFoundHandler = new Handler(
        handlerFunc: (BuildContext context, Map> params) {
      	print("ROUTE WAS NOT FOUND !!!");
    });
    router.define(root, handler: rootHandler);
    router.define(demoSimple, handler: demoRouteHandler, transitionType: TransitionType.inFromLeft);    
  }
}

// app_component.dart 文件  代码有省略
class AppComponentState extends State {
  AppComponentState() {
    final router = new Router();
    Routes.configureRoutes(router);
    Application.router = router;
  }

  @override
  Widget build(BuildContext context) {
    final app = new MaterialApp(
      title: 'Fluro',
      debugShowCheckedModeBanner: false,
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      onGenerateRoute: Application.router.generator,  // 生成路由
    );
    print("initial route = ${app.initialRoute}");
    return app;
  }
}

// 手动调用路由功能
String route = "/demo?message=$message&color_hex=$hexCode";
TransitionType transitionType = TransitionType.native;  // TransitionType.inFromLeft  fadeIn
Application.router
          .navigateTo(context, route, transition: transitionType)
          .then((result) {
        		if (key == "pop-result") {
          			Application.router.navigateTo(context, "/demo/func?message=$result");
        		}
      	  });

屏幕适配

可以使用 flutter_screenutil

根据设计图的尺寸进行适配,可以适配宽度、高度、字体大小,传入整型数值,按照比例换算后返回浮点数值。

框架的 Flex/Row/Column -> Expended 可以设置 flex 。


dependencies:
flutter:
    sdk: flutter
  # 添加依赖
  flutter_screenutil: ^0.5.0

import 'package:flutter_screenutil/flutter_screenutil.dart';

// 默认 width : 1080px , height:1920px , allowFontScaling:false
ScreenUtil.instance = ScreenUtil(width: 750, height: 1334, allowFontScaling: true)..init(context);

// 根据屏幕宽度适配 width: 
ScreenUtil.getInstance().setWidth(540);
ScreenUtil.getInstance().setHeight(200);
ScreenUtil().setHeight(200);

// 传入字体大小
ScreenUtil.getInstance().setSp(28) ;
ScreenUtil(allowFontScaling: true).setSp(28);   

// 相关的属性
ScreenUtil.pixelRatio       //设备的像素密度
ScreenUtil.screenWidth      //设备宽度
ScreenUtil.screenHeight     //设备高度
ScreenUtil.bottomBarHeight  //底部安全区距离,适用于全面屏下面有按键的
ScreenUtil.statusBarHeight  //状态栏高度 刘海屏会更高  单位px
ScreenUtil.textScaleFactory //系统字体缩放比例

ScreenUtil.getInstance().scaleWidth  // 实际宽度的dp与设计稿px的比例
ScreenUtil.getInstance().scaleHeight // 实际高度的dp与设计稿px的比例

消息提示

可以使用 fluttertoast


fluttertoast: ^3.0.3
flutter packages get

import 'package:fluttertoast/fluttertoast.dart';

Fluttertoast.showToast(
   msg: "This is a Toast 这是测试",
   toastLength: Toast.LENGTH_SHORT,    // Toast.LENGTH_LONG
   gravity: ToastGravity.BOTTOM,    // TOP CENTER 
   timeInSecForIos: 1,
   backgroundColor: Color(0x99000000),
   textColor: Colors.white,
   fontSize: 16.0
);

Fluttertoast.cancel();

网络请求

可以使用 dio

详细的中文文档,还有丰富的例子,忍不住点了 star 。


dependencies:
  dio: ^2.1.x  // 请使用pub上2.1分支的最新版本
  
import 'package:dio/dio.dart';

Response response;
Dio dio = new Dio();

// 配置 dio 实例
dio.options.baseUrl = "https://www.xx.com/api";    // 如果请求的路径以 http 开头,则忽略
dio.options.connectTimeout = 5000;     // 5s
dio.options.receiveTimeout = 3000;
dio.options.contentType = ContentType.parse("application/x-www-form-urlencoded");

// 或者通过传递一个 `options` 来创建 dio 实例
Options options = new BaseOptions(
    baseUrl: "https://www.xx.com/api",
    connectTimeout: 5000,
    receiveTimeout: 3000,
);
// Dio dio = new Dio(options);

response = await dio.get("/test?id=12&name=wendu");
print(response.data.toString());

// 请求参数也可以通过对象传递,上面的代码等同于:
response = await dio.get("/test", queryParameters: {"id": 12, "name": "wendu"});
print(response.data.toString());

response = await dio.post("/test", data: {"id": 12, "name": "wendu"});

response = await dio.download("https://www.google.com/", "./xx.html");

FormData formData = new FormData.from({
    "name": "wendux",
    "age": 25,
  });
response = await dio.post("/info", data: formData);

// Dio 实例的核心 API 是 :
Future request(String path, {data, Map queryParameters, Options options, CancelToken cancelToken, 	  
    ProgressCallback onSendProgress, ProgressCallback onReceiveProgress);

response = await request(
    "/test",
    data: {"id": 12, "name": "xx"},
    options: Options(method: "GET"),
);

// 请求配置
// method baseUrl headers path contentType responseType validateStatus queryParameters

// 响应数据 Response
// data headers request statusCode isRedirect

// 泛型支持 指定响应数据类型
dio.options.responseType=ResponseType.plain;
Response response = await dio.get("/test", options: Options(responseType: ResponseType.plain));
Response response = await dio.get("/test");

// 拦截器
dio.interceptors.add(InterceptorsWrapper(
    onRequest:(RequestOptions options){     
     return options;     // continue
     
     // 如果你想完成请求并返回一些自定义数据,可以返回一个`Response`对象或返回`dio.resolve(data)`    
     // 如果你想终止请求并触发一个错误,你可以返回一个`DioError`对象,或返回`dio.reject(errMsg)`
     return dio.resolve("fake data");     
    },
    onRequest:(Options options) async {       
        Response response = await dio.get("/token");      
        options.headers["token"] = response.data["data"]["token"];
		
		// 需要串行化请求/响应的场景中
		dio.interceptors.requestLock.lock();    // dio.lock()  dio.clear() 
		dio.interceptors.requestLock.unlock();

        return options;     // continue
    },
    onResponse:(Response response) {    
     return response;     // continue
    },
    onError: (DioError e) {     
     return e;    // continue
    }
));

// 日志  拦截器队列的执行顺序是 FIFO 建议把 log 拦截添加到队尾
dio.interceptors.add(LogInterceptor(responseBody: false));     // 开启请求日志

// Cookie 管理
// dio 默认使用 CookieJar , 它会将cookie保存在内存中。 
// 如果您想对 cookie 进行持久化, 请使用 PersistCookieJar
dio.interceptors.add(CookieManager(CookieJar())); 

// FormData
FormData formData = new FormData.from({
    "name": "wendux",
    "age": 25,
    "file": new UploadFileInfo(new File("./example/upload.txt"), "upload.txt")
});
response = await dio.post("/info", data: formData);

// 转换器
// 强烈建议 json 的解码通过 compute 方法在后台进行
_parseAndDecode(String response) {
  return jsonDecode(response);
}
parseJson(String text) {
  return compute(_parseAndDecode, text);
}
void main() {  
  (dio.transformer as DefaultTransformer).jsonDecodeCallback = parseJson;
  runApp(MyApp());
}

// 执行流
// 请求拦截器 >> 请求转换器 >> 发起请求 >> 响应转换器 >> 响应拦截器 >> 最终结果

// HttpClientAdapter
// Dio 和 HttpClient 之间的桥梁

// 设置 Http 代理
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {    
    client.findProxy = (uri) {
        //proxy all request to localhost:8888
        return "PROXY localhost:8888";
    };
    // you can also create a new HttpClient to dio
    // return new HttpClient();
};

// Https 证书校验  请求取消  其它的可以看官方的

自定义图标

可以使用 fluttericon
图标的素材也可以使用 iconfont

选择好图标,或者上传好图标后,设置名字,点击下载按钮,可以下载转换后的字体文件和代码,就可以在项目中使用了。

不需要的图标库要删除,还有框架自带的图标 uses-material-design: false

高德地图

可以使用高德地图的 Flutter 插件包,文档 ,API

图片和视频上传

调用原生功能获取图片或视频,可以使用 image_picker


dependencies:
  image_picker: ^0.5.3+1

$ flutter packages get

import 'package:image_picker/image_picker.dart';

Future getImage() async {
  var image = await ImagePicker.pickImage(source: ImageSource.camera);

  setState(() {
    _image = image;    // File _image;  Image.file(_image);
  });
}

日期选择

日期选择可以使用 flutter_picker
一个通过滚动或滑动选择的功能,可以用来选择数字、日期、数组或关联的数组。


flutter_picker:
  git: git://github.com/yangyxd/flutter_picker.git

import 'package:flutter_picker/flutter_picker.dart';

final GlobalKey _scaffoldKey = GlobalKey();
showPicker(BuildContext context) {

    Picker(
        cancelText: "取消",
        confirmText: "保存",
        adapter: DateTimePickerAdapter(type: 7, isNumberMonth: true),
        title: Text("选择日期"),
        textAlign: TextAlign.left,
        textStyle: const TextStyle(color: Colors.blue),
        onConfirm: (Picker picker, List value) {
          DateTime time = (picker.adapter as DateTimePickerAdapter).value;
          String year = time.year.toString();
          String month = time.month > 9 ? time.month.toString() : "0" + time.month.toString();
          String day = time.day > 9 ? time.day.toString() : "0" + time.day.toString();          
           setState(() {
              _value = "${year+'-'+month+'-'+day}";
           });
       	},
    ).show(_scaffoldKey.currentState);

	Picker(
        changeToFirst: true,
        adapter: PickerDataAdapter(data: dataList),
        title: Text("选择城市"),
        cancelText: "取消",
        confirmText: "保存",
        textAlign: TextAlign.left,
        textStyle: const TextStyle(color: Colors.blue),
        onConfirm: (Picker picker, List value) {         
          var arry = picker.getSelectedValues();
          provinceGuid = arry[0];
          provinceName = dataDicList.firstWhere((c) => c[arry[0]] != null)[arry[0]];
          cityGuid = arry[1];
          cityName = dataDicList.firstWhere((c) => c[arry[1]] != null)[arry[1]];
          regionGuid = arry[2];
          regionName = dataDicList.firstWhere((c) => c[arry[2]] != null)[arry[2]];
          setState(() {
            userInfo["provinceGuid"] = provinceGuid;
            userInfo["provinceName"] = provinceName;
            userInfo["cityGuid"] = cityGuid;
            userInfo["cityName"] = cityName;
            userInfo["regionGuid"] = regionGuid;
            userInfo["regionName"] = regionName;
          });
        }
    ).show(_scaffoldKey.currentState);
    
}

权限授权

手机的权限授权插件,permission_handler

输出项目日志和捕获错误报告

使用框架的功能开发的。


import 'dart:io';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';

/// ### 记录项目日志、捕获项目错误、缓存所有记录、写入文件、上传服务器
/// * 初始化:
/// ```dart
/// ErrorLog.log = new ErrorLog(
///     reportZone: () async {
///         runApp(new MyApp());
///     },
///     debugMode: true,
///     uploadFile: (file) async {},
///     minutesWait: 30,
///     [fileName: 'error_log.txt']
/// );
/// ```
/// 
/// ### 项目日志
/// 默认标记的项目日志可以使用相应的方法;自定义标记的项目日志可以使用基础方法。
/// * 使用方式:
/// ```dart
/// ErrorLog.log.debug('msg'*8);
/// ErrorLog.log.info('msg'*8);
/// ErrorLog.log.warn('msg'*8);
/// ErrorLog.log.error('msg'*8);
/// ErrorLog.log.fatal('msg'*8);
/// ErrorLog.log.collectLog('msg'*8, 'error');  // 都是调用这个基础方法
/// ```
/// * 输出格式:
/// #### [2019-04-18 11:50:29.844858][error] msgmsgmsgmsgmsgmsgmsgmsg
/// 
/// ### 错误报告
/// 错误报告的信息比较多,标记为`report`。
/// * 使用方式:
/// 自动捕获错误,不包含 `try/catch`,不包含 `print`。
/// * 输出格式:
/// #### [2019-04-18 14:05:03.578755][report]
/// #### 所有错误信息
/// 
/// ### 写入文件
/// 所有记录都缓存在一个数组里,如果`debugMode`为真,打印到控制台;
/// 否则根据数组索引异步写入文件`error_log.txt`,在初始化时可传参`fileName`。
/// * 使用方式:
/// ```dart
/// ErrorLog.log.printBuffer();  // 打印记录缓存
/// ErrorLog.log.clearFile();    // 清空记录文件
/// ErrorLog.log.printFile();    // 打印文件内容
/// ```
/// 
/// ### 上传服务器
/// 打开应用时上传一次,然后设置计时器,建议30分钟上传一次。
/// * 使用方式:
/// 初始化时传参`uploadFile`和`minutesWait`,获取记录的文件`ErrorLog.log.logFile`。
/// 
class ErrorLog {

  /// 实例,静态属性
  static ErrorLog log;

  /// 捕获错误的区域
  Function _reportZone;

  /// 是否调试模式
  bool _debugMode;

  /// 缓存记录的数组
  List _logBuffer;

  /// 文件记录
  File _logFile;
  File get logFile => _logFile;

  /// 文件记录的起点
  int _startIndex;

  /// 文件记录的终点
  int _endIndex;

  /// 上传记录文件
  Function _uploadFile;

  /// 上传时间间隔
  int _minutesWait;

  /// 记录文件是否变化
  bool _fileChange;

  /// 记录文件的名称
  String fileName;

  ErrorLog({
    @required Function reportZone, 
    @required bool debugMode,
    @required Function uploadFile,
    @required int minutesWait,
    this.fileName = 'error_log.txt'
  }) {
    
    _reportZone = reportZone;
    _debugMode = debugMode;
    _uploadFile = uploadFile;
    _minutesWait = minutesWait;
    
    init();

  }

  /// 初始化
  void init() async {

    _logBuffer = [];
    _startIndex = 0;
    _endIndex = 0;
    _fileChange = false;

    FlutterError.onError = (FlutterErrorDetails details) {
      reportError(details);
    };

    runZoned(
      _reportZone, 
      onError: (Object obj, StackTrace stack) {
        var details = makeDetails(obj, stack);
        reportError(details);
      }
    );

    _logFile = await _getLocalFile();
    _uploadFile(_logFile);
    Timer.periodic(Duration(minutes: _minutesWait), (timer) async {
      if (_fileChange) {
        await _uploadFile(_logFile);
        _fileChange = false;
      }
    });

  }


  /// 错误报告
  void reportError(FlutterErrorDetails details) {

    String errorMeta = '[' + (new DateTime.now().toString()) + '][report]';
    _logBuffer.add(errorMeta + '\n' + details.toString());

    if (_debugMode) {
      print(errorMeta);
      print(details.toString());
    } else {
      _writeFile();
    }
    
  }

  /// 项目日志
  collectLog(String line, String label) {

    String contents = '[' + (new DateTime.now().toString()) + '][' + label + '] ' + line;
    _logBuffer.add(contents + '\n');

    if (_debugMode) {
      print(contents);
    } else {
      _writeFile();
    }
    
  }


  /// 打印文件
  Future printFile() async {
    _readLocalFile().then((contents) {
      print(contents);
    });
  }

  /// 打印缓存
  Future printBuffer() async {
    print( _logBuffer.toString() );
  }

  /// 清空文件
  Future clearFile() async {
    await _logFile.writeAsString('', mode: FileMode.write);
  }

  /// 实时写入文件,防止意外
  Future _writeFile() async {

    int len = _logBuffer.length;
    if (len > _endIndex) {
      _startIndex = _endIndex;
      _endIndex = len;
      Iterable range = _logBuffer.getRange(_startIndex, _endIndex);
      await _writeLocalFile( range.join('\n') );
      _fileChange = true;
    } 

  }

  /// 获取文件
  Future _getLocalFile() async {
    String dir = (await getApplicationDocumentsDirectory()).path;
    return new File('$dir/' + fileName);
  }

  /// 读取文件
  Future _readLocalFile() async {
    String contents = await _logFile.readAsString();
    return contents;
  }

  /// 写入文件
  Future _writeLocalFile(String contents) async {
    await _logFile.writeAsString(contents, mode: FileMode.append, flush: false);
  }


  /// 构建错误信息
  FlutterErrorDetails makeDetails(Object obj, StackTrace stack) {
    return FlutterErrorDetails(exception: obj, stack: stack);
  }

  /// 调试
  void debug(String msg) {
    collectLog(msg, 'debug');
  }

  /// 信息
  void info(String msg) {
    collectLog(msg, 'info');
  }

  /// 警告
  void warn(String msg) {
    collectLog(msg, 'warn');
  }

  /// 错误
  void error(String msg) {
    collectLog(msg, 'error');
  }

  /// 致命错误
  void fatal(String msg) {
    collectLog(msg, 'fatal');
  }

}

注意事项

1、安卓模拟器在调试的时候,可能会出现旧的组件,可以停止调试,卸载模拟器上的项目,再重新调试;
2、定义一个类时,写好注释方便使用,注释符号使用 /// [] * `` ,解释一下,每行的开头符号、引用其它类名、列表项符号、引用代码;
3、使用组件时,多用用右键的重构功能,减少被晕的次数;
4、组件更新后,想要真正渲染到界面需要修改 key,因为更新组件的条件是判断新旧组件的 runtimeTypekey 是否有变化;

你可能感兴趣的:(Flutter,APP)