Flutter 实现手机端 App,如果想利用 AI 模型添加新颖的功能,那么 ncnn 就是一种可考虑的手机端推理模型的框架。

本文即是 Flutter 上使用 ncnn 做模型推理的实践分享。有如下内容:

  • ncnn 体验:环境准备、模型转换及测试
  • Flutter 项目体验: 本文 demo_ncnn 体验
  • Flutter 项目实现
    • 创建 FFI plugin,实现 dart 绑定 C 接口
    • 创建 App,于 Linux 应用 plugin 做推理
    • 适配 App,于 Android 能编译运行

demo_ncnn 代码: https://github.com/ikuokuo/start-flutter/tree/main/demo_ncnn

ncnn 体验

ncnn 环境准备

获取 ncnn 源码,并编译。以下是 Ubuntu 上的步骤:

# demo 用的预编译库,建议与其版本一致
export YYYYMMDD=20230517
git clone -b $YYYYMMDD --depth 1 https://github.com/Tencent/ncnn.git # Build for Linux
# https://github.com/Tencent/ncnn/wiki/how-to-build#build-for-linux
sudo apt install build-essential git cmake libprotobuf-dev protobuf-compiler libvulkan-dev vulkan-tools libopencv-dev cd ncnn/
git submodule update --init mkdir -p build; cd build # cmake -LAH ..
cmake -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=$HOME/ncnn-$YYYYMMDD \
-DNCNN_VULKAN=ON \
-DNCNN_BUILD_EXAMPLES=ON \
-DNCNN_BUILD_TOOLS=ON \
.. make -j$(nproc); make install

配置 ncnn 环境,

# 软链,以便替换
sudo ln -sfT $HOME/ncnn-$YYYYMMDD /usr/local/ncnn cat <<-EOF >> ~/.bashrc
# ncnn
export NCNN_HOME=/usr/local/ncnn
export PATH=\$NCNN_HOME/bin:\$PATH
EOF # 测试 tools
ncnnoptimize

测试 YOLOX 推理样例,

# 下载 YOLOX ncnn 模型,解压进工作目录 ncnn/build/examples
# 说明可见 ncnn/examples/yolox.cpp 的注释
# https://github.com/Megvii-BaseDetection/YOLOX/releases/download/0.1.1rc0/yolox_s_ncnn.tar.gz
tar -xzvf yolox_s_ncnn.tar.gz # 下载 YOLOX 测试图片,拷贝进工作目录 ncnn/build/examples
# https://github.com/Megvii-BaseDetection/YOLOX/blob/main/assets/dog.jpg # 进入工作目录
cd ncnn/build/examples # 运行 YOLOX ncnn 样例
./yolox dog.jpg

ncnn 模型转换

上述 YOLOX 推理,用的是已转换好的模型。实际推理某一个模型,得了解如何做转换。

这里还以 YOLOX 模型为例,体验 ncnn 转换、修改、量化模型的过程。步骤依照的 YOLOX/demo/ncnn 的说明。此外,ncnn/tools 下有各类模型转换工具的说明。

Step 1) 下载 YOLOX 模型

Step 2) onnx2ncnn 转换模型

# onnx 简化
# https://github.com/daquexian/onnx-simplifier
# pip3 install onnxsim
python3 -m onnxsim yolox_nano.onnx yolox_nano_sim.onnx # onnx 转换为 ncnn
onnx2ncnn yolox_nano_sim.onnx yolox_nano.param yolox_nano.bin

报错 Unsupported slice step ! 可忽略。Focus layer 已经于 demo 的 yolox.cpp 里实现了。

Step 3) 修改 yolox_nano.param

修改 yolox_nano.param 把第一个 Convolution 前的层都删掉,另加个 YoloV5Focus 层,并修改层数值。

修改前:

291 324
Input images 0 1 images
Split splitncnn_input0 1 4 images images_splitncnn_0 images_splitncnn_1 images_splitncnn_2 images_splitncnn_3
Crop 630 1 1 images_splitncnn_3 630 -23309=2,0,0 -23310=2,2147483647,2147483647 -23311=2,1,2
Crop 635 1 1 images_splitncnn_2 635 -23309=2,0,1 -23310=2,2147483647,2147483647 -23311=2,1,2
Crop 640 1 1 images_splitncnn_1 640 -23309=2,1,0 -23310=2,2147483647,2147483647 -23311=2,1,2
Crop 650 1 1 images_splitncnn_0 650 -23309=2,1,1 -23310=2,2147483647,2147483647 -23311=2,1,2
Concat Concat_40 4 1 630 640 635 650 683 0=0
Convolution Conv_41 1 1 683 1177 0=16 1=3 11=3 2=1 12=1 3=1 13=1 4=1 14=1 15=1 16=1 5=1 6=1728

修改后:

286 324
Input images 0 1 images
YoloV5Focus focus 1 1 images 683

注:onnx 简化这里用处不大,合了本来要删除的几个 Crop 层。

Step 4) ncnnoptimize 量化模型

ncnnoptimize 转为 fp16,减少一半权重:

ncnnoptimize yolox_nano.param yolox_nano.bin yolox_nano_fp16.param yolox_nano_fp16.bin 65536

如果量化为 int8,可见 Post Training Quantization Tools

ncnn 推理实践

修改 ncnn/examples/yolox.cpp detect_yolox() 里模型路径,重编译后测试:

cd ncnn/build/examples
./yolox dog.jpg

demo_ncnn 体验

demo_ncnn 是本文实践的演示项目,可以运行体验。效果如下:

准备 Flutter 环境

Flutter 请依照官方文档 Get started 进行准备。

准备 demo_ncnn 项目

获取 demo_ncnn 源码,

git clone --depth 1 https://github.com/ikuokuo/start-flutter.git

其中,

  • demo_ncnn/: 选择图片进行 ncnn 推理的 Flutter 应用
  • plugins/ncnn_yolox/: ncnn 推理 yolox 模型的 Flutter FFI 插件

安装依赖,

cd demo_ncnn/

flutter pub get

sudo apt-get install libclang-dev libomp-dev

准备 Linux 预编译库,

  • ncnn: ncnn-YYYYMMDD-ubuntu-2204-shared.zip
  • opencv: opencv-mobile-4.6.0-ubuntu-2204.zip

解压进 plugins/ncnn_yolox/linux/

准备 Android 预编译库,

  • ncnn: ncnn-YYYYMMDD-android-vulkan-shared.zip
  • opencv: opencv-mobile-4.6.0-android.zip

解压进 plugins/ncnn_yolox/android/

确认 ncnn_yolox/src/CMakeLists.txtncnn_DIR OpenCV_DIR 的路径正确。

体验 demo_ncnn 项目

运行体验,

cd demo_ncnn/
flutter run # 或查看设备,-d 指定运行
flutter devices
flutter run -d linux

demo_ncnn 实现

demo_ncnn 实现,分为两部分:

  • Flutter FFI 插件:实现 dart 绑定 C 接口
  • Flutter App 应用:实现 UI 并应用插件做推理

创建 FFI 插件

# 创建 FFI 插件
flutter create --org dev.flutter -t plugin_ffi --platforms=android,ios,linux ncnn_yolox cd ncnn_yolox # 更新 ffigen 版本
# 不然,可能报错 Error: The type 'YoloX' must be 'base', 'final' or 'sealed'
flutter pub outdated
flutter pub upgrade --major-versions

之后,只需在 src/ncnn_yolox.h 里定义 C 接口并实现,然后用 package:ffigen 自动生成 Dart 绑定就可以了。

Step 1) 定义 C 接口

src/ncnn_yolox.h

#ifdef __cplusplus
extern "C" {
#endif FFI_PLUGIN_EXPORT typedef int yolox_err_t; #define YOLOX_OK 0
#define YOLOX_ERROR -1 FFI_PLUGIN_EXPORT struct YoloX {
const char *model_path; // path to model file
const char *param_path; // path to param file float nms_thresh; // nms threshold
float conf_thresh; // threshold of bounding box prob
float target_size; // target image size after resize, might use 416 for small model
}; // ncnn::Mat::PixelType
FFI_PLUGIN_EXPORT enum PixelType {
PIXEL_RGB = 1,
PIXEL_BGR = 2,
PIXEL_GRAY = 3,
PIXEL_RGBA = 4,
PIXEL_BGRA = 5,
}; FFI_PLUGIN_EXPORT struct Rect {
float x;
float y;
float w;
float h;
}; FFI_PLUGIN_EXPORT struct Object {
int label;
float prob;
struct Rect rect;
}; FFI_PLUGIN_EXPORT struct DetectResult {
int object_num;
struct Object *object;
}; FFI_PLUGIN_EXPORT struct YoloX *yoloxCreate();
FFI_PLUGIN_EXPORT void yoloxDestroy(struct YoloX *yolox); FFI_PLUGIN_EXPORT struct DetectResult *detectResultCreate();
FFI_PLUGIN_EXPORT void detectResultDestroy(struct DetectResult *result); FFI_PLUGIN_EXPORT yolox_err_t detectWithImagePath(
struct YoloX *yolox, const char *image_path, struct DetectResult *result);
FFI_PLUGIN_EXPORT yolox_err_t detectWithPixels(
struct YoloX *yolox, const uint8_t *pixels, enum PixelType pixelType,
int img_w, int img_h, struct DetectResult *result); #ifdef __cplusplus
}
#endif

Step 2) 实现 C 接口

src/ncnn_yolox.cc 实现参考 ncnn/examples/yolox.cpp 来做的。

Step 3) 更新 Dart 绑定接口

lib/ncnn_yolox_bindings_generated.dart

flutter pub run ffigen --config ffigen.yaml

如果要了解 dart 怎么与 C 交互,可见:C interop using dart:ffi

Step 4) 准备依赖库

准备 ncnn opencv 的预编译库,

  • Linux,解压进 linux/

    • ncnn-YYYYMMDD-ubuntu-2204-shared.zip
    • opencv-mobile-4.6.0-ubuntu-2204.zip
  • Android,解压进 android/
    • ncnn-YYYYMMDD-android-vulkan-shared.zip
    • opencv-mobile-4.6.0-android.zip

Step 5) 写构建脚本

src/CMakeLists.txt

# packages

if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(ncnn_DIR "${MY_PROJ}/linux/ncnn-20230517-ubuntu-2204-shared/lib/cmake")
set(OpenCV_DIR "${MY_PROJ}/linux/opencv-mobile-4.6.0-ubuntu-2204/lib/cmake")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Android")
set(ncnn_DIR "${MY_PROJ}/android/ncnn-20230517-android-vulkan-shared/${ANDROID_ABI}/lib/cmake/ncnn")
set(OpenCV_DIR "${MY_PROJ}/android/opencv-mobile-4.6.0-android/sdk/native/jni")
else()
message(FATAL_ERROR "system not support: ${CMAKE_SYSTEM_NAME}")
endif() if(NOT EXISTS ${ncnn_DIR})
message(FATAL_ERROR "ncnn_DIR not exists: ${ncnn_DIR}")
endif()
if(NOT EXISTS ${OpenCV_DIR})
message(FATAL_ERROR "OpenCV_DIR not exists: ${OpenCV_DIR}")
endif() ## ncnn find_package(ncnn REQUIRED)
message(STATUS "ncnn_FOUND: ${ncnn_FOUND}") ## opencv find_package(OpenCV 4 REQUIRED)
message(STATUS "OpenCV_VERSION: ${OpenCV_VERSION}")
message(STATUS "OpenCV_INCLUDE_DIRS: ${OpenCV_INCLUDE_DIRS}")
message(STATUS "OpenCV_LIBS: ${OpenCV_LIBS}") # targets include_directories(
${MY_PROJ}/src
${OpenCV_INCLUDE_DIRS}
) ## ncnn_yolox add_library(ncnn_yolox SHARED
"ncnn_yolox.cc"
)
target_link_libraries(ncnn_yolox ncnn ${OpenCV_LIBS}) set_target_properties(ncnn_yolox PROPERTIES
PUBLIC_HEADER ncnn_yolox.h
OUTPUT_NAME "ncnn_yolox"
) target_compile_definitions(ncnn_yolox PUBLIC DART_SHARED_LIB)

测试 ncnn 推理

首先,把准备好的模型放进 assets 目录。如:

assets/
├── dog.jpg
├── yolox_nano_fp16.bin
└── yolox_nano_fp16.param

之后,于 Linux 可以自测 C & Dart 接口实现。

Step 1) C 接口测试

linux/ncnn_yolox_test.cc

std::string assets_dir("../assets/");
std::string image_path = assets_dir + "dog.jpg";
std::string model_path = assets_dir + "yolox_nano_fp16.bin";
std::string param_path = assets_dir + "yolox_nano_fp16.param"; auto yolox = yoloxCreate();
yolox->model_path = model_path.c_str();
yolox->param_path = param_path.c_str();
yolox->nms_thresh = 0.45;
yolox->conf_thresh = 0.25;
yolox->target_size = 416;
// yolox->target_size = 640; auto detect_result = detectResultCreate(); auto err = detectWithImagePath(yolox, image_path.c_str(), detect_result);
if (err == YOLOX_OK) {
auto num = detect_result->object_num;
printf("yolox detect ok, num=%d\n", num);
for (int i = 0; i < num; i++) {
Object *obj = detect_result->object + i;
printf(" object[%d] label=%d prob=%.2f rect={x=%.2f y=%.2f w=%.2f h=%.2f}\n",
i, obj->label, obj->prob, obj->rect.x, obj->rect.y, obj->rect.w, obj->rect.h);
}
} else {
printf("yolox detect fail, err=%d\n", err);
} draw_objects(image_path.c_str(), detect_result); detectResultDestroy(detect_result);
yoloxDestroy(yolox);

Step 2) Dart 接口测试

linux/ncnn_yolox_test.dart

final yoloxLib = NcnnYoloxBindings(dlopen('ncnn_yolox', 'build/shared'));

const assetsDir = '../assets';
final imagePath = '$assetsDir/dog.jpg'.toNativeUtf8();
final modelPath = '$assetsDir/yolox_nano_fp16.bin'.toNativeUtf8();
final paramPath = '$assetsDir/yolox_nano_fp16.param'.toNativeUtf8(); final yolox = yoloxLib.yoloxCreate();
yolox.ref.model_path = modelPath.cast();
yolox.ref.param_path = paramPath.cast();
yolox.ref.nms_thresh = 0.45;
yolox.ref.conf_thresh = 0.25;
yolox.ref.target_size = 416;
// yolox.ref.target_size = 640; final detectResult = yoloxLib.detectResultCreate(); final err =
yoloxLib.detectWithImagePath(yolox, imagePath.cast(), detectResult); if (err == YOLOX_OK) {
final num = detectResult.ref.object_num;
print('yolox detect ok, num=$num');
for (int i = 0; i < num; i++) {
var obj = detectResult.ref.object.elementAt(i).ref;
print(' object[$i] label=${obj.label}'
' prob=${obj.prob.toStringAsFixed(2)} rect=${obj.rect.str()}');
}
} else {
print('yolox detect fail, err=$err');
} calloc.free(imagePath);
calloc.free(modelPath);
calloc.free(paramPath); yoloxLib.detectResultDestroy(detectResult);
yoloxLib.yoloxDestroy(yolox);

Step 3) 运行测试

cd ncnn_yolox/linux
make # cpp test
./build/ncnn_yolox_test # dart test
dart ncnn_yolox_test.dart

创建 App 写 UI

创建 App 项目,

flutter create --project-name demo_ncnn --org dev.flutter --android-language java --ios-language objc --platforms=android,ios,linux demo_ncnn

本文项目添加了如下些依赖:

cd demo_ncnn

dart pub add path logging image easy_debounce

flutter pub add mobx flutter_mobx provider path_provider
flutter pub add -d build_runner mobx_codegen

App 状态管理用的 MobX。若要了解使用,可见:

App 主要就两个功能:选图片、做推理。对应实现了两个 Store 类:

因为加载、预测都比较耗时,故用的 MobX ObservableFuture 异步方式。若要了解使用,可见:

以上就是 App 实现的关键内容,也可采取不同方案。

应用插件做推理

App 里应用插件,首先要于 pubspec.yaml 里加上插件的依赖:

dependencies:
ncnn_yolox:
path: ../plugins/ncnn_yolox

然后,yolox_store.dart 应用了插件做推理,过程与之前 Dart 接口测试基本一致。差异主要在:

  • 多了将 assets 里的模型拷贝进临时路径的操作,因为 App 里无法获取资源的绝对路径。要么改 C 接口,模型以字节给到。
  • 多了将图片数据从 Uint8ListPointer<Uint8> 的拷贝,因为要从 Dart 堆内存进 C 堆内存。可见注释的 Issue 了解。
import 'dart:ffi';
import 'dart:io'; import 'package:ffi/ffi.dart';
import 'package:flutter/services.dart';
import 'package:image/image.dart' as img;
import 'package:mobx/mobx.dart'; import 'package:ncnn_yolox/ncnn_yolox_bindings_generated.dart' as yo;
import 'package:path/path.dart' show join;
import 'package:path_provider/path_provider.dart'; import '../util/image.dart';
import '../util/log.dart';
import 'future_store.dart'; part 'yolox_store.g.dart'; class YoloxStore = YoloxBase with _$YoloxStore; class YoloxObject {
int label = 0;
double prob = 0;
Rect rect = Rect.zero;
} class YoloxResult {
List<YoloxObject> objects = [];
Duration detectTime = Duration.zero;
} abstract class YoloxBase with Store {
late yo.NcnnYoloxBindings _yolox; YoloxBase() {
final dylib = Platform.isAndroid || Platform.isLinux
? DynamicLibrary.open('libncnn_yolox.so')
: DynamicLibrary.process(); _yolox = yo.NcnnYoloxBindings(dylib);
} @observable
FutureStore<YoloxResult> detectFuture = FutureStore<YoloxResult>(); @action
Future detect(ImageData data) async {
try {
detectFuture.errorMessage = null; detectFuture.future = ObservableFuture(_detect(data)); detectFuture.data = await detectFuture.future;
} catch (e) {
detectFuture.errorMessage = e.toString();
}
} Future<YoloxResult> _detect(ImageData data) async {
final timebeg = DateTime.now();
// await Future.delayed(const Duration(seconds: 5)); final modelPath = await _copyAssetToLocal('assets/yolox_nano_fp16.bin',
package: 'ncnn_yolox', notCopyIfExist: false);
final paramPath = await _copyAssetToLocal('assets/yolox_nano_fp16.param',
package: 'ncnn_yolox', notCopyIfExist: false);
log.info('yolox modelPath=$modelPath');
log.info('yolox paramPath=$paramPath'); final modelPathUtf8 = modelPath.toNativeUtf8();
final paramPathUtf8 = paramPath.toNativeUtf8(); final yolox = _yolox.yoloxCreate();
yolox.ref.model_path = modelPathUtf8.cast();
yolox.ref.param_path = paramPathUtf8.cast();
yolox.ref.nms_thresh = 0.45;
yolox.ref.conf_thresh = 0.45;
yolox.ref.target_size = 416;
// yolox.ref.target_size = 640; final detectResult = _yolox.detectResultCreate(); final pixels = data.image.getBytes(order: img.ChannelOrder.bgr);
// Pass Uint8List to Pointer<Void>
// https://github.com/dart-lang/ffi/issues/27
// https://github.com/martin-labanic/camera_preview_ffi_image_processing/blob/master/lib/image_worker.dart
final pixelsPtr = calloc.allocate<Uint8>(pixels.length);
for (int i = 0; i < pixels.length; i++) {
pixelsPtr[i] = pixels[i];
} final err = _yolox.detectWithPixels(
yolox,
pixelsPtr,
yo.PixelType.PIXEL_BGR,
data.image.width,
data.image.height,
detectResult); final objects = <YoloxObject>[];
if (err == yo.YOLOX_OK) {
final num = detectResult.ref.object_num;
for (int i = 0; i < num; i++) {
final o = detectResult.ref.object.elementAt(i).ref;
final obj = YoloxObject();
obj.label = o.label;
obj.prob = o.prob;
obj.rect = Rect.fromLTWH(o.rect.x, o.rect.y, o.rect.w, o.rect.h);
objects.add(obj);
}
} calloc
..free(pixelsPtr)
..free(modelPathUtf8)
..free(paramPathUtf8); _yolox.detectResultDestroy(detectResult);
_yolox.yoloxDestroy(yolox); final result = YoloxResult();
result.objects = objects;
result.detectTime = DateTime.now().difference(timebeg);
return result;
} // ...
}

最后,于 UI home_page.dart 里使用,

class HomePage extends StatefulWidget {
const HomePage({super.key, required this.title}); final String title; @override
State<HomePage> createState() => _HomePageState();
} class _HomePageState extends State<HomePage> {
late ImageStore _imageStore;
late YoloxStore _yoloxStore;
late OptionStore _optionStore; @override
void didChangeDependencies() {
_imageStore = Provider.of<ImageStore>(context);
_yoloxStore = Provider.of<YoloxStore>(context);
_optionStore = Provider.of<OptionStore>(context); _imageStore.load(); super.didChangeDependencies();
} void _pickImage() async {
final result = await FilePicker.platform.pickFiles(type: FileType.image);
if (result == null) return; final image = result.files.first;
_imageStore.load(imagePath: file.path);
} void _detectImage() {
if (_imageStore.loadFuture.futureState != FutureState.loaded) return;
_yoloxStore.detect(_imageStore.loadFuture.data!);
} @override
Widget build(BuildContext context) {
const pad = 20.0;
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Padding(
padding: const EdgeInsets.all(pad),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 图片与结果
Expanded(
flex: 1,
child: Observer(builder: (context) {
if (_imageStore.loadFuture.futureState ==
FutureState.loading) {
return const Center(child: CircularProgressIndicator());
} if (_imageStore.loadFuture.errorMessage != null) {
return Center(
child: Text(_imageStore.loadFuture.errorMessage!));
} final data = _imageStore.loadFuture.data;
if (data == null) {
return const Center(child: Text('Image load null :('));
} _yoloxStore.detectFuture.reset(); return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.orangeAccent)),
child: DetectResultPage(imageData: data),
);
})),
const SizedBox(height: pad),
// 三个按钮:选图、推理、是否显示框
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: ElevatedButton(
child: const Text('Pick image'),
onPressed: () => _debounce('_pickImage', _pickImage),
),
),
const SizedBox(width: pad),
Expanded(
child: ElevatedButton(
child: const Text('Detect objects'),
onPressed: () => _debounce('_detectImage', _detectImage),
),
),
const SizedBox(width: pad),
Expanded(
child: Observer(builder: (context) {
return ElevatedButton.icon(
icon: Icon(_optionStore.bboxesVisible
? Icons.check_box_outlined
: Icons.check_box_outline_blank),
label: const Text('Binding boxes'),
onPressed: () => _optionStore
.setBboxesVisible(!_optionStore.bboxesVisible),
);
}),
),
],
),
],
),
),
);
}
}

适配 Android 工程

Android 构建脚本在 android/build.gradle,也用的 CMake,与 Linux 共享了 src/CMakeLists.txt。不过要把 minSdkVersion 改成 24,以使用 Vulkan。

Vulkan 于 Android 7.0 (Nougat), API level 24 or higher 开始支持,可见 NDK / Get started with Vulkan

plugins/ncnn_yolox/android/build.gradle 配置:

android {
defaultConfig {
minSdkVersion 24
ndk {
moduleName "ncnn_yolox"
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
}
}

demo_ncnn/android/app/build.gradle 也一样修改 minSdkVersion24

最后,即可 flutter run 运行。更多可见 Build and release an Android app

适配 iOS 工程

本文项目未适配 iOS。如何适配 iOS,请见:

Xcode 14 不再支持提交含有 bitcode 的应用,Flutter 3.3.x 之后也移除了 bitcode 的支持,可见 Creating an iOS Bitcode enabled app

更多参考

Flutter ncnn 使用的更多相关文章

  1. 修改ncnn的openmp异步处理方法 附C++样例代码

    ncnn刚发布不久,博主在ios下尝试编译. 遇上了openmp的编译问题. 寻找各种解决方案无果,亲自操刀. 采用std::thread 替换 openmp. ncnn项目地址: https://g ...

  2. Flutter 初尝:从 Java 无缝过渡

    准备阶段 下载 Flutter SDK 新建 Flutter 文件夹,克隆 Flutter SDK: git clone -b beta https://github.com/flutter/flut ...

  3. 编写第一个Flutter App(翻译)

    博客搬迁至http://blog.wangjiegulu.com RSS订阅:http://blog.wangjiegulu.com/feed.xml 以下代码 Github 地址:https://g ...

  4. 【译】Java、Kotlin、RN、Flutter 开发出来的 App 大小,你了解过吗?

    现在开发 App 的方式非常多,原生.ReactNative.Flutter 都是不错的选择.那你有没有关注过,使用不同的方式,编译生成的 Apk ,大小是否会有什么影响呢?本文就以一个最简单的 He ...

  5. 我花了 8 小时,"掌握"了一下 Flutter | Flutter 中文站上线

    Hi,大家好,我是承香墨影! 距离 Google 在 2018 世界移动大会上发布 Flutter 的 Beta 版本,Flutter 是 Google 用以帮助开发者在 Android 和 iOS ...

  6. flutter初体验

    flutter初体验 和flutter斗争了两个周末,基本弄清楚了这个玩意的布局和一些常用组件了. 在flutter里面,所有东西都是组件Widget.我们像拼接积木一样拼接Widget,拼接的关键词 ...

  7. Flutter 实现原理及在马蜂窝的跨平台开发实践

    一直以来,跨平台开发都是困扰移动客户端开发的难题. 在马蜂窝旅游 App 很多业务场景里,我们尝试过一些主流的跨平台开发解决方案, 比如 WebView 和 React Native,来提升开发效率和 ...

  8. Flutter 即学即用系列博客——05 StatelessWidget vs StatefulWidget

    前言 上一篇我们对 Flutter UI 有了一个基本的了解. 这一篇我们通过自定义 Widget 来了解下如何写一个 Widget? 然而 Widget 有两个,StatelessWidget 和 ...

  9. Flutter 异常处理之图片篇

    背景 说到异常处理,你可能直接会认为不就是 try-catch 的事情,至于写一篇文章单独来说明吗? 如果你是这么想的,那么本篇说不定会给你惊喜哦~ 而且本篇聚焦在图片的异常处理. 场景 学以致用,有 ...

  10. Flutter 即学即用系列博客——07 RenderFlex overflowed 引发的思考

    背景 在进行 Flutter UI 开发的时候,控制台报出了下面错误: flutter: ══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY >╞════════ ...

随机推荐

  1. 界面重建——Marching cubes算法

    一.引子 对于一个标量场数据,我们可以描绘轮廓(Contouring),包括2D和3D.2D的情况称为轮廓线(contour lines),3D的情况称为表面(surface).他们都是等值线或等值面 ...

  2. 深谈Spring如何解决Bean的循环依赖

    1. 什么是循环依赖 Java循环依赖指的是两个或多个类之间的相互依赖,形成了一个循环的依赖关系,这会导致程序编译失败或运行时出现异常.下面小岳就带大家来详细分析下Java循环依赖. 简单来讲就是:假 ...

  3. 2021年蓝桥杯python真题-路径(数论+动态规划)(LCM、GCD和DP详细介绍)干货满满~

    欢迎大家阅读本文章 如果大家对LCM和GCD不是很熟悉,这篇文章将对你有帮助! 本文章也会把动态规划做一定的介绍 题目: GCD和LCM的讲解: GCD的实现-辗转相除法: 在数学中,辗转相除法,又称 ...

  4. vue下载附件按钮功能

    一.tools文件夹下tools文件中封装下载方法: const iframeId = 'download_file_iframe' function iframeEle (src) { let el ...

  5. 2022-12-21:uifd/ui-for-docker是docker的web可视化工具。请问部署在k3s中,yaml文件如何写?

    2022-12-21:uifd/ui-for-docker是docker的web可视化工具.请问部署在k3s中,yaml文件如何写? 答案2022-12-21: yaml如下: apiVersion: ...

  6. 2022-01-17:单词规律 II。 给你一种规律 pattern 和一个字符串 str,请你判断 str 是否遵循其相同的规律。 这里我们指的是 完全遵循,例如 pattern 里的每个字母和字符

    2022-01-17:单词规律 II. 给你一种规律 pattern 和一个字符串 str,请你判断 str 是否遵循其相同的规律. 这里我们指的是 完全遵循,例如 pattern 里的每个字母和字符 ...

  7. 2022-01-10:路径交叉。给你一个整数数组 distance 。 从 X-Y 平面上的点 (0,0) 开始,先向北移动 distance[0] 米,然后向西移动 distance[1] 米,向南

    2022-01-10:路径交叉.给你一个整数数组 distance . 从 X-Y 平面上的点 (0,0) 开始,先向北移动 distance[0] 米,然后向西移动 distance[1] 米,向南 ...

  8. Django4全栈进阶之路20 项目实战(三种方式开发部门管理):方式一:FBV

    1.模型 from django.db import models from django.contrib.auth.models import User # Create your models h ...

  9. 图数据库 NebulaGraph 的内存管理实践之 Memory Tracker

    数据库的内存管理是数据库内核设计中的重要模块,内存的可度量.可管控是数据库稳定性的重要保障.同样的,内存管理对图数据库 NebulaGraph 也至关重要. 图数据库的多度关联查询特性,往往使图数据库 ...

  10. 深入理解 python 虚拟机:魔术方法之数学计算

    深入理解 python 虚拟机:魔术方法之数学计算 在本篇文章当中主要给大家介绍在 python 当中一些常见的魔术方法,本篇文章主要是关于与数学计算相关的一些魔术方法,在很多科学计算的包当中都使用到 ...