给Android工程师的Flutter入门手册(二)

1,362 阅读6分钟

前言

这是笔者作为一个Android工程师入门Flutter的学习笔记,笔者不想通过一种循规蹈矩的方式来学习:先学Dart语言,然后学习Flutter的基本使用,再到实践应用这样的步骤。这样的方式有点无趣且效率较低。

笔者觉得对于已经有Android基础的来说,通过类比Android的方式来学习Flutter,掌握核心基础概念后,直接开发实践应用,在这个过程中去学习其中的知识比如Dart语法、深入的知识点。这是笔者的一次学习尝试,并将其记录下来:

给Android工程师的Flutter入门手册(一)

本篇是该系列的第二篇,主要内容是:

(1)工程结构和资源文件:在哪里放置分辨率相关的图片文件?字符串存储在哪里?

(2)Activity和Fragment

(3)数据库和本地存储

资源文件和资产文件

Android 中是区分对待资源文件 (resources) 和资产文件 (assets)的。

Flutter 应用只有资产文件 (assets)。所有原本在 Android 中应该放在 res/drawable-* 文件夹中的资源文件,在 Flutter 中都放在一个assets文件夹中。

Flutter放置图片资源

Flutter 遵循一个简单的类似iOS的密度相关的格式。文件可以是一倍 (1x)、两倍 (2x)、三倍 (3x) 或其它的任意倍数。 Flutter 没有 dp 单位,但是有逻辑像素尺寸,基本和设备无关的像素尺寸是一样的。

区分逻辑像素和设备像素:

逻辑像素也称为与设备无关或与分辨率无关的像素。设备像素也称为物理像素。

 devicePixelRatio 表示在单一逻辑像素标准下设备物理像素的比例。或者可以理解成,显示此视图屏幕的每个逻辑像素的设备像素数。 devicePixelRatio 返回的值最终是从硬件本身、设备驱动程序或存储在操作系统或固件中的硬编码值获得的,并且可能不准确,有时误差很大。  PS:Flutter 框架以逻辑像素为单位进行操作,因此很少需要直接处理该属性。

Android的密度分类与 Flutter 像素比例的对照表如下:

Android的密度Flutter 像素比例
ldpi0.75x
mdpi1.0x
hdpi1.5x
xhdpi2.0x
xxhdpi3.0x
xxxhdpi4.0x

如果在 Flutter 项目中添加一个新的叫 my_icon.png 的图片资源,并且将其放入我们随便起名的叫做 images 的任意文件夹中。Flutter 没有预先定义好的文件夹结构。你在 pubspec.yaml 文件中定义文件(包括位置信息),Flutter 负责找到它们。

你需要将基础图片(1.0x)放在 images 文件夹中,并将其它倍数的图片放入以特定倍数作为名称的子文件夹中:

images/my_icon.png       // Base: 1.0x image
images/2.0x/my_icon.png  // 2.0x image
images/3.0x/my_icon.png  // 3.0x image

并且需要在 pubspec.yaml 文件中定义这些图片:

flutter:  
  assets:  
    - images/my_image.jpg

PS:这里注意assets的缩进格式,缩进有问题会出现Unable to load asset问题

如果要包含一个目录下的所有 assets,需要在目录名称的结尾加上 /

flutter:  
  assets:  
    - images/

如果想要添加子文件夹中的文件,请为每个目录创建一个条目。

字符串储存在哪里?

Flutter 当下并没有一个特定的管理字符串的资源管理系统。目前来讲,最好的办法是将字符串作为静态域存放在类中,并通过类访问它们。例如:

class Strings {
  static String welcomeMessage = 'Welcome To Flutter';
}

接着在代码中可以这样访问字符串:

Text(Strings.welcomeMessage);

当然,这样只能处理一些本地化的语言,如果想处理多语言场景,这种方式处理起来就很吃力了。走国际化路线可以使用 intl 包 进行国际化和本地化。关于这部分不是本篇博客关键,后续有需要单独开一篇博客介绍。

Gradle 文件的对应物是什么?我该如何添加依赖?

AndroidGradle非常重要,我们在 Gradle 构建脚本中添加依赖。 那么在Flutter中,我们是使用 Dart 自己的构建系统以及 Pub 包管理器。构建工具会将原生 Android iOS 壳应用的构建代理给对应的构建系统。

虽然在Flutter 项目的 android 文件夹下有 Gradle 文件,但是它们只用于给对应平台的集成添加原生依赖。可以在 pubspec.yaml 文件中定义在 Flutter 里使用的外部依赖。

Activiy和Fragment

Activity 和 Fragment 在 Flutter 中的对应什么?

在 Android 中,一个 Activity 代表用户可以完成的一件独立任务。一个 Fragment 代表一个行为或者用户界面的一部分。 Fragment 用于模块化你的代码,为大屏组合复杂的用户界面,并适配应用的界面。 正如 Flutter 中一切皆为 Widget,这两个概念也都对应于 Widget

如何监听Android Activity 的生命周期事件

Android 中,可以通过覆写 Activity 的生命周期方法来监听其生命周期,也可以在 Application 上注册 ActivityLifecycleCallbacks

Flutter 中,这两种方法都没有,但是你可以通过绑定 WidgetsBinding 观察者并监听 didChangeAppLifecycleState() 的变化事件来监听生命周期。

一个监听生命周期的代码示例:

import 'package:flutter/widgets.dart';

class LifecycleWatcher extends StatefulWidget {
  const LifecycleWatcher({super.key});

  @override
  State<LifecycleWatcher> createState() => _LifecycleWatcherState();
}

class _LifecycleWatcherState extends State<LifecycleWatcher>
    with WidgetsBindingObserver {
  AppLifecycleState? _lastLifecycleState;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    setState(() {
      _lastLifecycleState = state;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_lastLifecycleState == null) {
      return const Text(
        'This widget has not observed any lifecycle changes.',
        textDirection: TextDirection.ltr,
      );
    }

    return Text(
      'The most recent lifecycle state this widget observed was: $_lastLifecycleState.',
      textDirection: TextDirection.ltr,
    );
  }
}

void main() {
  runApp(const Center(child: LifecycleWatcher()));
}

虽然 FlutterActivity 在内部捕获了几乎所有的 Activity 生命周期事件并将它们发送给 Flutter 引擎,但是它们大部分都向你屏蔽了。

可以被观察的生命周期事件有:

  • inactive:应用处于非活跃状态并且不接收用户输入。
  • detached :应用依然保留 flutter engine,但是全部宿主 view 均已脱离。
  • paused :应用当前对用户不可见,无法响应用户输入,并运行在后台。这个事件对应于 Android 中的 onPause()
  • resumed :应用对用户可见并且可以响应用户的输入。这个事件对应于 Android 中的 onPostResume()
  • suspending :应用暂时被挂起。这个事件对应于 Android 中的 onStop; iOS 上由于没有对应的事件,因此不会触发此事件。

Flutter 为你管理引擎的启动和停止,在大部分情况下没有理由要在 Flutter 一端监听 Activity 的生命周期。 如果你需要通过监听生命周期来获取或释放原生的资源,是应该在原生端做这件事的。

数据库和本地存储

本地存储

Android 中,可以使用 SharedPreferences API 来存储少量的键值对。

Flutter 中,使用 Shared_Preferences 插件 实现此功能。这个插件同时包装了 Shared PreferencesNSUserDefaults(iOS 平台对应 API)的功能。但是数据可能会异步持久化到磁盘,不保证写入返回后一定会持久化到磁盘,所以这个插件一定不要用于存储关键数据。

举个简单的示范代码:添加一个按钮计数,每次在存储值的基础上+1

import 'dart:async';  
import 'package:flutter/material.dart';  
  
import 'package:shared_preferences/shared_preferences.dart';  
  
void main() {  
  runApp(  
    const MaterialApp(  
      home: Scaffold(  
        body: Center(  
          child: ElevatedButton(  
            onPressed: _incrementCounter,  
            child: Text('Increment Counter'),  
          ),  
        ),  
      ),  
    ),  
  );  
}  
  
Future<void> _incrementCounter() async {  
  SharedPreferences prefs = await SharedPreferences.getInstance();  
  int counter = (prefs.getInt('counter') ?? 0) + 1;  
  debugPrint("counter: $counter");  
  await prefs.setInt('counter', counter);  
}

最后在data文件夹的应用包名下有个FlutterSharedPreferences.xml文件,存储了SP的数据

这是我的路径:
/data/data/com.example.flutter_enter_door/shared_prefs/FlutterSharedPreferences.xml

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>  
<map>  
    <long name="flutter.counter" value="6" />  
</map>

数据库

Android 中,你会使用 SQLite 来存储可以通过 SQL 进行查询的结构化数据。

Flutter 中,使用 SQFlite 插件实现此功能。该插件支持支持 iOS、Android 和 MacOS:

  • 支持事务和批处理
  • 打开时自动进行版本管理
  • 插入/查询/更新/删除查询的助手
  • 在 iOS 和 Android 的后台线程中执行的数据库操作

此外,其他平台支持:

  • 使用 sqflite_common_ffi 的 Linux/Windows/DartVM 支持
  • 使用 sqflite_common_ffi_web 的实验性 Web 支持。

举个狗子相关数据库的例子:

定义狗子bean:

class Dog {  
  
  final int id;  
  final String name;  
  final int age;  
  
  const Dog({  
    required this.id,  
    required this.name,  
    required this.age,  
  });  
  
  Map<String, dynamic> toMap(){  
    return {  
      'id': id,  
      'name': name,  
      'age': age,  
    };  
  }  
  
  @override  
  String toString() {  
    return 'Dog{id: $id, name: $name, age: $age}';  
  }  
  
}

创建数据库和狗子表:

void createDogTable() async {  
  WidgetsFlutterBinding.ensureInitialized();  
  database = openDatabase(  
    // 创建数据库  
    join(await getDatabasesPath(), 'doggie_database.db'),  
    onCreate: (db, version) {  
      // 创建dogs表  
      return db.execute(  
        'CREATE TABLE dogs(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)',  
      );  
    },  
    version: 1,  
  );  
}

狗子数据的增删改查:

// 新增一条狗子数据  
Future<void> insertDog(Dog dog) async {  
  final db = await database;  
  await db.insert(dogTableName, dog.toMap(),  
      conflictAlgorithm: ConflictAlgorithm.replace);  
}  
  
// 查询狗子数据  
Future<List<Dog>> getDogs() async {  
  final db = await database;  
  final List<Map<String, dynamic>> maps = await db.query(dogTableName);  
  return List.generate(maps.length, (i) {  
    return Dog(id: maps[i]['id'], name: maps[i]['name'], age: maps[i]['age']);  
  });  
}  
  
// 删除狗子数据  
Future<void> deleteDog(int id) async {  
  // Get a reference to the database.  
  final db = await database;  
  
  // Remove the Dog from the database.  
  await db.delete(  
    dogTableName,  
    // Use a `where` clause to delete a specific dog.  
    where: 'id = ?',  
    // Pass the Dog's id as a whereArg to prevent SQL injection.  
    whereArgs: [id],  
  );  
}  
  
// 更新狗子数据  
Future<void> updateDog(Dog dog) async {  
  // Get a reference to the database.  
  final db = await database;  
  
  // Update the given Dog.  
  await db.update(  
    dogTableName,  
    dog.toMap(),  
    // Ensure that the Dog has a matching id.  
    where: 'id = ?',  
    // Pass the Dog's id as a whereArg to prevent SQL injection.  
    whereArgs: [dog.id],  
  );  
}

参考

1.字符串部分

intl包:intl

2.生命周期部分

AppLifecycleState: AppLifecycleState enum

3.数据存储部分:

用 SQLite 做数据持久化

使用 Database Inspector 调试数据库