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. vue自定义密码输入框解决浏览器自动填充密码的问题

    pre { overflow-y: auto; max-height: 300px } img { max-width: 500px; max-height: 300px } 问题描述 浏览器对于ty ...

  2. Swift CustomStringConvertible 协议的使用

    目录 一.前言 二.使用场景 1. 整型类型的枚举使用 2. Class类型的使用 一.前言 先看一下Swift标准库中对CustomStringConvertible协议的定义 public pro ...

  3. Java双向链表实现队列

    将双向链表做简单的改造,即可实现一个FIFO(First Input First Out)队列, 该队列只在头节点出队,尾节点入队. 一般来说定义节点类只需一个后驱节点next即可. 这里保留pre节 ...

  4. Swift WisdomProtocol 面向协议编程(下)

    WisdomProtocol 面向协议编程(下) @[TOC] WisdomProtocol SDK 面向协议编程 # Welcome to use WisdomProtocol WisdomProt ...

  5. 2020-12-03:mysql中,Heap 表是什么?

    福哥答案2020-12-04:[答案来自此链接:](http://bbs.xiangxueketang.cn/question/605) Heap表,即使用MEMORY存储引擎的表,这种表的数据存储在 ...

  6. 2021-01-13:很多列的数据,任意一列组合查询,mysql能做到,但是上亿的数据量做不到了,查的时候非常慢。我们需要一个引擎来支持它。这个引擎你有了解过吗?

    福哥答案2021-01-13:[答案来自此链接:](https://www.zhihu.com/question/439121902)数据库存储设计一般分为行存储还有列存储.行存储一般每一行的数据通过 ...

  7. 2021-09-11:给你一个32位的有符号整数x,返回将x中的数字部分反转后的结果。反转后整数超过 32 位的有符号整数的范围就返回0,假设环境不允许存储 64 位整数(有符号或无符号)。

    2021-09-11:给你一个32位的有符号整数x,返回将x中的数字部分反转后的结果.反转后整数超过 32 位的有符号整数的范围就返回0,假设环境不允许存储 64 位整数(有符号或无符号). 福大大 ...

  8. Golang接收者方法语法糖

    1.概述 在<Golang常用语法糖>这篇博文中我们讲解Golang中常用的12种语法糖,在本文我们主要讲解下接收者方法语法糖. 在介绍Golang接收者方法语法糖前,先简单说下Go 语言 ...

  9. IDP 与 DevOps平台:相似之处与关键差异

    软件开发是一个复杂而动态的过程,涉及许多工具.技术和实践.为了更快.更好地交付软件,开发人员需要有效地协作,自动执行任务,并管理环境.然而,由于软件架构的日益复杂,工具和平台的多样性,以及对安全和合规 ...

  10. ubuntu搜狗输入法显示简体中文,输入却是繁体中文问题解决方案

    一.现场重现 我的ubuntu版本是20.04,搜狗输入法版本是2.4.在输入的时候发生了如下场景: 明明输入法上是简体中文,可是打出来就变成了繁体中文! 二.解决方案 1.尝试 网上许多答案都是按下 ...