关于 how-to-build-a-native-cross-platform-project-with-flutter
介绍如何用 Flutter 构建面向移动端、Web 和桌面的项目,并通过条件导入选择平台实现及回退方案。
主要功能
- 01Flutter 跨平台项目设置
- 02按平台条件导入
- 03不支持平台的回退实现
- 04按平台打开 URL
适用场景
- 01在移动端和 Web 间共享 Flutter 代码
- 02将导入路由到平台专属实现
- 03在 Web 和移动端采用不同方式打开 URL
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rodydavis/ skills --skill how-to-build-a-native-cross-platform-project-with-flutter安装后在 Claude Code 中输入/how-to-build-a-native-cross-platform-project-with-flutter确认已加载。
详细介绍
如何使用 Flutter 构建原生跨平台项目
在同一个项目中导入 dart:html 和 dart:io!
简而言之:最终源码在这里。
到目前为止,你可以使用 Flutter 创建可在 iOS/Android、Web 和桌面端运行的项目,但只能共享纯 Dart 插件。
Flutter 在 Google I/O 上推出了 Flutter for web,当时它是一个临时分支,要求你将导入语句从 import 'package:flutter/material.dart'; 改为 import 'package:flutter_web/material.dart';。
你可以想象,这对代码库来说确实很麻烦,因为你必须创建一个分支并修改导入语句。这也意味着你无法导入需要在某个路径上运行或依赖 Flutter 的任何软件包。现在,合并终于完成了。你不再需要修改导入语句!
![]()
现在你可以使用任何插件、运行调试器、创建包含 Web 文件夹的新 Flutter 项目、使用 Web 插件,等等……
免责声明
你需要使用最新版本的 Flutter 才能正常运行。
![]()
如果你刚接触 Flutter,可以查看这份实用指南,了解如何逐步创建新项目。
![]()
创建一个名为 flutter_x 的新项目,它应该如下所示:
![]()
你也可以在这里下载起始项目。
你的代码应该如下所示:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
为了确保一切正常,请运行项目并在 iOS/Android 上进行测试。
![]()
你应该能看到计数器应用正在运行,且工作正常。现在退出,然后在 Chrome 上运行。它应该会显示在设备列表中。你也可以从命令行运行 flutter run -d chrome。
![]()
请注意,Web 端暂时还不支持热重载。
![]()
现在你的项目应该如下所示。
打开 pubspec.yaml 并导入以下软件包。
dependencies:
universal_html:
url_launcher:
你也可以删除 pubspec.yaml 中自动生成的注释。
此时,你的 pubspec.yaml 应如下所示:
name: flutter_x
description: A new Flutter project.
version: 1.0.0+1
environment:
sdk: ">=2.1.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
universal_html: ^1.1.0
url_launcher: ^5.1.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
默认情况下,如果你要检查设备是移动设备还是 Web 设备,在尝试导入不适用于该平台的插件时,会在编译时出错。为了解决这个问题,我们将使用动态导入。
![]()
在 plugins 文件夹中创建一个 url_launcher 文件夹,并在其中创建 url_launcher.dart、mobile.dart、web.dart 和 unsupported.dart 文件。
在 url_launcher.dart 文件中添加以下内容:
export 'unsupported.dart'
if (dart.library.html) 'web.dart'
if (dart.library.io) 'mobile.dart';
这样会在运行时选择正确的文件,并在不受支持时提供回退方案。
为避免边缘情况,你需要为导入设置回退方案。在 unsupported.dart 中添加以下内容:
class UrlUtils {
UrlUtils._();
static void open(String url, {String name}) {
throw 'Platform Not Supported';
}
}
要使其正常工作,三个文件中的 UrlUtils 类及其公开方法必须保持一致。请务必先设置 unsupported.dart,然后将该文件复制到 mobile.dart 和 web.dart,以确保没有拼写错误。
现在,你应该有 3 个文件,每个文件中的类都包含上述代码。
在 mobile.dart 中添加以下内容:
import 'package:url_launcher/url_launcher.dart';
class UrlUtils {
UrlUtils._();
static void open(String url, {String name}) async {
if (await canLaunch(url)) {
await launch(url);
}
}
}
这会分别在 Safari 视图控制器或 Android 默认浏览器中打开链接。
在 web.dart 中添加以下内容:
import 'package:universal_html/prefer_universal/html.dart' as html;
class UrlUtils {
UrlUtils._();
static void open(String url, {String name}) {
html.window.open(url, name);
}
}
这将在浏览器中打开一个新窗口,并转到指定链接。
在屏幕中央添加一个按钮。ui/home/screen.dart 应如下所示:
import 'package:flutter/material.dart';
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: RaisedButton(
child: Text('Open Flutter.dev'),
onPressed: () {},
)),
);
}
}
将 onPressed 更新为以下内容:
onPressed: () {
try {
UrlUtils.open('[https://flutter.dev'](https://flutter.dev'));
} catch (e) {
print('Error -> $e');
}
},
现在导入 UrlUtils 时,请务必导入正确的 URI。
![]()
请确保仅导入 import 'package:flutter_x/plugins/url_launcher/url_launcher.dart';。
你也可以使用相对导入。
你的 UI 代码现在应如下所示:
import 'package:flutter/material.dart';
import '../../plugins/url_launcher/url_launcher.dart';
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: RaisedButton(
child: Text('Open Flutter.dev'),
onPressed: () {
try {
UrlUtils.open('[https://flutter.dev'](https://flutter.dev'));
} catch (e) {
print('Error -> $e');
}
},
)),
);
}
}
你的应用在 web 上应如下所示:
![]()
点击按钮后……
![]()
在 iOS/Android 上运行时,应用应如下所示:
![]()
点击按钮后……
![]()
恭喜!你完成了 🎉
![]()
最终项目位于此处。
如有任何问题,请随时联系!