Flutter学习笔记

中文开发者网站中文开发者社区 官网网页版预览Flutter

  • 完整的Demo流程 ✅
  • 常用的组件有哪些 ✅
  • 页面导航 ✅
  • 包依赖管理 ✅
  • 资源引用 ✅
  • 数据持久化 ✅
  • 网络请求 ✅
  • json解析 ✅
  • 原生调用的Flutter模块 ✅
  • 开发Flutter包和插件 ✅

完整的Demo流程

  • ChangeNotifier 设置应用状态。
  • ChangeNotifierProvider 应用状态提供给整个应用,任何组件都可以获取状态。
  • SafeArea确保显示不会被凹槽的状态栏挡住。
  • [WordPair? pair]加[]表示是可选的参数。

常用组件

  • MaterialApp 主题

  • Scaffold 脚手架

  • NavigationRail 导航组件

  • LayoutBuilder 布局伸缩通知变化

  • SizedBox、Spacer空白占用

  • Padding 间距

  • Text文本

  • Icon图标

  • TextButton.icon 文本+图标 按钮

  • Row和Column 行列列表

  • GridView 网格,ListTile网格项

页面导航

go路由库

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
39
40
41
42
//0.安装&依赖
flutter pub add go_router
dependencies:
go_router: ^14.3.0
//1.定义路由
final GoRouter _router = GoRouter(
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const HomeScreen();
},
routes: <RouteBase>[
GoRoute(
path: 'details',
builder: (BuildContext context, GoRouterState state) {
return const DetailsScreen();
},
),
],
),
],
);
class MyApp extends StatelessWidget {
/// Constructs a [MyApp]
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
);
}
}
//2.页面定义
class HomeScreen extends StatelessWidget {...}
class DetailsScreen extends StatelessWidget {...}
//3.路由跳转
child: ElevatedButton(
onPressed: () => context.go('/'),
child: const Text('Go back to the Home screen'),
)

资源引用

1
2
3
4
//pubspec.yaml 配置文件
assets:
- assets/images/
- assets/json/
  • 分辨率 1.0x、2.0x这种方式引用,v3.24.3会提示找不到异常,先忽略。
  • svg加载
    1
    2
    3
    4
    flutter pub add flutter_svg
    //加载
    import 'package:flutter_svg/flutter_svg.dart';
    SvgPicture.asset('assets/images/a4.svg',semanticsLabel: "icon",)
  • 字符串国际化
    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
    //1.搜索插件 Flutter Intl
    //2.添加依赖
    dependencies:
    flutter_localizations:
    sdk: flutter
    intl: any
    flutter:
    generate: true
    //3.根目录新建文件l10n.yaml,arb就是json文件,添加以下内容:
    arb-dir: lib/l10n
    template-arb-file: app_en.arb
    output-localization-file: app_localizations.dart
    //4.运行 flutter run 命令,你将在 ${FLUTTER_PROJECT}/.dart_tool/flutter_gen/gen_l10n 中看到生成的文件。
    //5.入口添加
    import 'package:flutter_localizations/flutter_localizations.dart';
    import 'package:flutter_gen/gen_l10n/app_localizations.dart';
    MaterialApp(
    localizationsDelegates: [
    AppLocalizations.delegate,
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
    ],
    supportedLocales: [
    const Locale('en', 'US'), // 美国英语
    const Locale('zh', 'CN'), // 中文简体
    //其他Locales
    ],
    locale: const Locale('en', 'US'), //手动指定locale
    //6.获取当前语言
    Locale myLocale = Localizations.localeOf(context);
    //7.引用字符串
    Text(AppLocalizations.of(context)!.welcome)

数据持久化

  • key-values
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //执行
    flutter pub add shared_preferences
    //自动添加依赖
    shared_preferences: ^2.3.2
    //获取String
    final prefs = await SharedPreferences.getInstance();
    spDataText = prefs.getString(Constants.SP_NAME) ?? '';
    //设置String
    final prefs = await SharedPreferences.getInstance();
    prefs.setString(Constants.SP_NAME, data);
    //移除String
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(Constants.SP_NAME);
  • sqlite
    1
    2
    3
    4
    5
    //执行
    flutter pub add sqflite path
    //自动添加依赖
    sqflite: ^2.4.0
    path: ^1.9.0

网络请求

1
2
flutter pub add http
import 'package:http/http.dart' as http;
  • 添加权限
    1
    2
    3
    4
    //android
    <uses-permission android:name="android.permission.INTERNET" />
    //macos/Runner/DebugProfile.entitlements 和 macos/Runner/Release.entitlements 文件添加网络权限
    <!-- Required to fetch data from the internet. --> <key>com.apple.security.network.client</key> <true/>
  • FutureBuilder 异步组件显示网络数据
    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
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    //Album数据类
    class Album {
    final int userId;
    final int id;
    final String title;

    Album({required this.userId, required this.id, required this.title});

    factory Album.fromJson(Map<String, dynamic> json) {
    if (json.containsKey('userId') &&
    json.containsKey('id') &&
    json.containsKey('title')) {
    return Album(
    userId: json['userId'] as int,
    id: json['id'] as int,
    title: json['title'] as String,
    );
    } else {
    throw const FormatException('Failed to load album.');
    }
    }
    }
    //http请求
    Future<Album> fetchAlbum() async{
    final response = await http
    .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));

    if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
    } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load album');
    }
    }
    //显示组件
    late Future<Album> futureAlbum = fetchAlbum();
    FutureBuilder<Album>(
    future: futureAlbum,
    builder: (context, snapshot) {
    if (snapshot.hasData) {
    return Text("title=${snapshot.data!.title},userId=${snapshot.data!.userId}");
    } else if (snapshot.hasError) {
    return Text('${snapshot.error}');
    }

    // By default, show a loading spinner.
    return const CircularProgressIndicator();
    },
    )

json解析

  • dart:convert:内置的库,手动序列化/反序列化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class User{
    String name = "";
    int age = 0;

    User();

    User.fromJson(Map<String, dynamic> json)
    : name = json['name'] as String,
    age = json['age'] as int;

    Map<String, dynamic> toJson() => {
    'name': name,
    'age': age,
    };
    }
    //反序列化
    var user = jsonDecode(json) as Map<String, dynamic>;
    print("test 名字:${user['name']},年龄:${user['age']}");
    //序列化
    var userObj = User();
    userObj.name = user['name'];
    userObj.age = user['age'];
    String json = jsonEncode(userObj);//是调用toJson方法。
  • dart:json_serializable:自动序列化/反序列化
    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
    //依赖
    flutter pub add json_annotation dev:build_runner dev:json_serializable
    //使用注解
    import 'package:json_annotation/json_annotation.dart';
    part 'user.g.dart';

    @JsonSerializable()
    class User{
    @JsonKey(name: 'name') //别名
    String name = "";
    int age = 0;

    User();

    factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

    Map<String, dynamic> toJson() => _$UserToJson(this);
    }
    //如果有dart安装不是flutter自带,使用brew删除
    brew uninstall dart
    //生成代码User.g.dart
    flutter pub run build_runner build --delete-conflicting-outputs
    //User类修改
    import 'package:json_annotation/json_annotation.dart';
    part 'User.g.dart'; //还是报错,但是可以运行起来✅

    @JsonSerializable(nullable: false)
    class User{
    @JsonKey(name: 'name') //别名
    String name = "";
    int age = 0;

    User();
    factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
    Map<String, dynamic> toJson() => _$UserToJson(this);

    }

原生调用的Flutter模块

Flutter模块

  • 导出aar包:flutter build aar

    • 如果直接导包,需要查看输出的文件夹下:pom文件类似这些依赖也一并导入
      1
      2
      3
      4
      <groupId>io.flutter</groupId>
      <artifactId>flutter_embedding_debug</artifactId>
      <version>1.0.0-36335019a8eab588c3c2ea783c618d90505be233</version>
      <scope>compile</scope>
    • 使用本地仓库依赖
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      allprojects {
      String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?:
      "https://storage.googleapis.com"
      repositories {
      maven {
      url'../../flutter_module/build/host/outputs/repo'
      }
      maven {
      url "$storageUrl/download.flutter.io"
      }
      google()
      mavenCentral()
      }
      }
      //添加依赖项
      debugImplementation 'dev.flutter.example.flutter_module:flutter_debug:1.0'
      releaseImplementation 'dev.flutter.example.flutter_module:flutter_release:1.0'
  • 配置Flutter模块

  • flutter create -t module flutter_module创建Flutter模块

    1
    2
    3
    4
    5
    6
    //在pubspec.yaml下配置为Flutter模块
    flutter:
    module:
    androidX: true
    androidPackage: dev.flutter.example.flutter_module //定义android模块下唯一标识
    iosBundleIdentifier: dev.flutter.example.flutterModule //定义ios模块下唯一标识
  • 集成到android或ios

    1
    2
    3
    4
    5
    6
    7
    8
    cd flutter_module/
    flutter pub get
    #android打开项目即可
    open -a "Android Studio" ../android_fullscreen
    #ios需要pod安装下再打开
    cd ../ios_fullscreen
    pod install
    open IOSFullScreen.xcworkspace

Android

  • FlutterEngine预初始化
  • MethodChannel
  • methodChannel#setMethodCallHandler(..) 处理flutter回调原生
  • methodChannel#invokeMethod(..) 处理原生调用flutter
  • FlutterActivity flutter页面
    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
    39
    40
    41
    42
    43
    const val ENGINE_ID = "1"
    private lateinit var channel: MethodChannel
    var count = 0
    /**Application#onCreate()下调用**/
    val flutterEngine = FlutterEngine(this)
    flutterEngine
    .dartExecutor
    .executeDartEntrypoint(
    DartExecutor.DartEntrypoint.createDefault()
    )

    FlutterEngineCache.getInstance().put(ENGINE_ID, flutterEngine)
    channel = MethodChannel(flutterEngine.dartExecutor, "dev.flutter.example/counter")
    channel.setMethodCallHandler { call, _ ->
    //flutter调用原生时,接收
    when (call.method) {
    "incrementCounter" -> {
    count++
    reportCounter()
    }
    "requestCounter" -> {
    reportCounter()
    }
    }
    }
    //...

    private fun reportCounter() {
    //原生调用flutter
    channel.invokeMethod("reportCounter", count)
    }
    //跳转到Flutter 页面
    val intent = FlutterActivity
    .withCachedEngine(ENGINE_ID)
    .build(this)
    startActivity(intent)
    //添加AndroidMainfest.xml声明
    <activity
    android:name="io.flutter.embedding.android.FlutterActivity"
    android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
    android:exported="true"
    android:hardwareAccelerated="true"
    android:windowSoftInputMode="adjustResize" />
  • android源码方式引入
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //build.gradle文件
    dependencies {
    implementation project(':flutter')
    }
    //settings.gradle文件,其中evaluate执行指定文件的代码
    setBinding(new Binding([gradle: this]))
    evaluate(new File(
    settingsDir.parentFile,
    'flutter_module/.android/include_flutter.groovy'
    ))
    include ':flutter_module'
    project(':flutter_module').projectDir = new File('../flutter_module')
  • android aar包方式引入
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //执行
    flutter build aar
    //添加gradle本地依赖库地址
    String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?:
    "https://storage.googleapis.com"
    repositories {
    maven {
    url
    '/Users/zhangxuyang/Desktop/Demo/add-to-app/flutter_module/build/hos
    t/outputs/repo'
    }
    maven {
    url "$storageUrl/download.flutter.io"
    }
    }
    //添加依赖库
    debugImplementation("dev.flutter.example.flutter_module:flutter_debug:1.0") releaseImplementation("dev.flutter.example.flutter_module:flutter_release:1.0")//
  • ⚠️使用kts时,新建flutter_settings.gradle,再引入
    1
    2
    3
    4
    5
    6
    apply { from("flutter_settings.gradle") }
    /**遇到这个问题
    Caused by: org.gradle.api.InvalidUserCodeException: Build was configured to prefer settings repositories over project repositories but repository ‘maven’ was added by plugin class ‘FlutterPlugin’ Caused by: org.gradle.api.internal.plugins.PluginApplicationException: Failed to apply plugin class ‘FlutterPlugin’.
    **/
    //修改repositoriesMode
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
    1
    2
    3
    4
    5
    //异常
    Caused by: org.gradle.api.internal.plugins.PluginApplicationException: Failed to apply plugin class 'FlutterPlugin'.
    //修改repositoriesMode
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)

Android-Error
  • 使用Compose新建项目导入flutter模块提示错误
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Execution failed for task ':app:checkDebugAarMetadata'.
    > Could not resolve all files for configuration ':app:debugRuntimeClasspath'.
    > Could not find org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0.
    Searched in the following locations:
    - https://storage.googleapis.com/download.flutter.io/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.9.0/kotlin-stdlib-jdk8-1.9.0.pom
    Required by:
    project :app
    > Could not find androidx.compose.ui:ui-tooling:.
    Required by:

Flutter

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
39
40
- `MethodChannel`
- `methodChannel#setMethodCallHandler(..)` 处理原生回调flutter
- `methodChannel#invokeMethod<void>(..)`处理flutter调用原生
```dart
void main() {
//这个调用确保,在MethodChannel创建之前,Flutter绑定关系已经设置好了。
WidgetsFlutterBinding.ensureInitialized();

final model = CounterModel();

runApp(
ChangeNotifierProvider.value(
value: model,
child: const MyApp(),
),
);
}

class CounterModel extends ChangeNotifier {
CounterModel() {
_channel.setMethodCallHandler(_handleMessage);
_channel.invokeMethod<void>('requestCounter');
}
final _channel = const MethodChannel('dev.flutter.example/counter');
int _count = 0;
int get count => _count;

void increment() {
//Flutter中点击触发,调用原生方法增加count。
_channel.invokeMethod<void>('incrementCounter');
}

Future<dynamic> _handleMessage(MethodCall call) async {
//处理原生调flutter方法,更新count。
if (call.method == 'reportCounter') {
_count = call.arguments as int;
notifyListeners();
}
}
}

iOS

  • FlutterEngine
  • flutterEngine?.run(withEntrypoint: nil)初始化启动Flutter引擎
  • FlutterMethodChannel
  • flutterMethodChannel#setMethodCallHandler(..) 设置flutter回调原生ios
  • flutterMethodChannel?.invokeMethod(..) 设置ios调用flutter
  • FlutterViewControllerflutter页面
  • iOS新建项目出现上下区域黑色,改配置里面Launch Screen File 添加LaunchScreen
    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
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    //应用入口初始化
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    var flutterEngine : FlutterEngine?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Instantiate Flutter engine
    self.flutterEngine = FlutterEngine(name: "io.flutter", project: nil)
    self.flutterEngine?.run(withEntrypoint: nil)

    return true
    }
    }
    //UIViewController下
    var methodChannel : FlutterMethodChannel?
    var count = 0

    if let flutterEngine = (UIApplication.shared.delegate as? AppDelegate)?.flutterEngine {
    methodChannel = FlutterMethodChannel(name: "dev.flutter.example/counter",binaryMessenger: flutterEngine.binaryMessenger)
    methodChannel?.setMethodCallHandler({ [weak self](call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
    if let strongSelf = self {
    switch(call.method) {
    case "incrementCounter":
    strongSelf.count += 1
    strongSelf.counterLabel.text = "Current counter: \(strongSelf.count)"
    strongSelf.reportCounter()
    case "requestCounter":
    strongSelf.reportCounter()
    default:
    // Unrecognized method name
    print("Unrecognized method name: \(call.method)")
    }
    }
    })
    }

    func reportCounter() {
    methodChannel?.invokeMethod("reportCounter", arguments: count)
    }
    @IBAction func buttonWasTapped(_ sender: Any) {
    if let flutterEngine = (UIApplication.shared.delegate as? AppDelegate)?.flutterEngine {
    let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
    self.present(flutterViewController, animated: true, completion: nil)
    }
    }

    //pod文件

    flutter_application_path = '../flutter_module'
    load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

    target 'IOSFullScreen' do
    # Comment the next line if you don't want to use dynamic frameworks
    use_frameworks!

    # Pods for IOSFullScreen
    install_all_flutter_pods(flutter_application_path)

    target 'IOSFullScreenTests' do
    inherit! :search_paths
    # Pods for testing
    end

    target 'IOSFullScreenUITests' do
    inherit! :search_paths
    # Pods for testing
    end

    end

    post_install do |installer|
    flutter_post_install(installer) if defined?(flutter_post_install)
    end

iOS-Error
  • 使用SwiftUI 新建项目 导入flutter模块 pod install提示错误
    [!] The platform of the target iOSAddFlutter(macOS 14.0) is not compatible withFlutterPluginRegistrant (0.0.1), which does not support macOS.

开发Flutter包和插件

纯Flutter的自定义包
  • 创建自定义包flutter create --template=package hello
  • 通过path路径形式加载包
    1
    2
    3
    dependencies: 
    hello:
    path: ../../packgage-plugin/hello/
  • 通过git形式加载包,(可通过path设置路径,默认是根目录下)
    1
    2
    3
    4
    5
    dependencies: 
    packageA:
    git:
    url: git@github.com:flutter/packageA.git
    path: packages/packageA

调用特定平台API的原生插件包

  • flutter create --template=plugin --platforms=android,ios -i objc hello,其中-i指定ios语言,-a指定android语言 ,平台有android,ios,linux,macos,windows
  • ErrorTips:新增–platforms的macos时,运行macos应用提示error: no such module 'native_hello' import native_hello。需要在macos重新运行 pod install重新刷新flutter插件。
  • Futurn.then(),调用异步代码