给Android工程师的Flutter入门手册(二)
前言
这是笔者作为一个Android
工程师入门Flutter
的学习笔记,笔者不想通过一种循规蹈矩的方式来学习:先学Dart
语言,然后学习Flutter
的基本使用,再到实践应用这样的步骤。这样的方式有点无趣且效率较低。
笔者觉得对于已经有Android
基础的来说,通过类比Android
的方式来学习Flutter
,掌握核心基础概念后,直接开发实践应用,在这个过程中去学习其中的知识比如Dart
语法、深入的知识点。这是笔者的一次学习尝试,并将其记录下来:
本篇是该系列的第二篇,主要内容是:
(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 像素比例 |
---|---|
ldpi | 0.75x |
mdpi | 1.0x |
hdpi | 1.5x |
xhdpi | 2.0x |
xxhdpi | 3.0x |
xxxhdpi | 4.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 文件的对应物是什么?我该如何添加依赖?
Android
中Gradle
非常重要,我们在 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 Preferences
和 NSUserDefaults
(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.数据存储部分:
转载自:https://juejin.cn/post/7243241586568609852