序言
上篇文章中,我们了解到了flutter中的weiget,Navigator,Isolate,http等等,他们分别对应了Android中的view,intent,Coroutine,okhttp,我们的学习已渐入佳境。本篇文章我们继续从Android视角认识Flutter。
Project structure & resources
项目结构和资源
我的分辨率相关的图像文件应该存储在哪里?
虽然 Android 将资源和资产视为不同的项目,但 Flutter 应用程序只有assets。所有在 Android 上会存放在 res/drawable-* 文件夹中的资源,在 Flutter 中都被放置在一个资产文件夹< assets folder>里。
Flutter 采用一种类似 iOS 的简单基于密度的格式。资源可能是 1.0 倍、2.0 倍、3.0 倍或其他任何倍数。Flutter 没有设备独立像素(dps),但有逻辑像素,其本质上与设备独立像素相同。Flutter 的** devicePixelRatio 表示单个逻辑像素中物理像素的比例**。
与安卓的密度桶相当的是:

资源可以放在任何任意文件夹中 ------Flutter 没有预定义的文件夹结构。你需要在 pubspec.yaml 文件中声明资源(包括其位置),Flutter 会自动获取这些资源。
存储在原生资源文件夹中的资源可在原生端使用 Android 的 AssetManager 进行访问:
注意,下面的代码是在Android中访问flutter项目中的资源
kotlin
val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")
Flutter 无法访问原生资源或资产。
例如,要向我们的 Flutter 项目中添加一个名为 my_icon.png 的新图像资源,并决定将其放在一个我们随意命名为 images 的文件夹中,你需要将基础图像(1.0 倍)放在 images 文件夹里,而所有其他变体则放在以相应比例乘数命名的子文件夹中:
kotlin
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 文件中声明这些图片:

然后你可以使用 AssetImage 访问你的图片:

或者使用下面的方式
kotlin
@override
Widget build(BuildContext context) {
return Image.asset('images/my_image.png');
}
我应该在哪里存储字符串?如何处理本地化?
Flutter 目前没有专门用于字符串的类资源系统。
最佳且推荐的做法是将字符串以键值对的形式保存在.arb 文件中。
默认情况下,Flutter 仅提供美式英语的本地化支持。要添加对其他语言的支持,应用程序必须指定额外的 MaterialApp(或 CupertinoApp)属性,并导入含一个名为 flutter_localizations 的包。
要使用 flutter_localizations,请将该包以及 intl 包作为依赖添加到您的 pubspec.yaml 文件中:
kotlin
flutter pub add flutter_localizations --sdk=flutter
flutter pub add intl:any
这会在 pubspec.yml 文件中创建以下条目的:
kotlin
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: any
打开 pubspec.yaml 文件并启用 generate 标志。该标志位于 pubspec 文件的 flutter 部分。

在 Flutter 项目的根目录下添加一个新的 yaml 文件。将该文件命名为 l10n.yaml,并包含以下内容:

在同一目录下添加另一个名为 app_es.arb 的捆绑文件。在这个文件中,添加同一条消息的西班牙语翻译。

现在,运行 flutter pub get 或 flutter run,代码生成会自动进行。你应该能在通过 arb-dir 或 output-dir 选项指定的路径对应的目录中找到生成的文件。另外,你也可以运行 flutter gen-l10n 来生成相同的文件,而无需运行应用程序。
然后导入 flutter_localizations 库,并为你的 MaterialApp 或 CupertinoApp 指定 localizationsDelegates 和 supportedLocales:

最后你就可以正常使用他了

Gradle 文件的等效文件是什么?我该如何添加依赖项?
在安卓系统中,你可以通过向 Gradle 构建脚本添加内容来添加依赖项。Flutter 使用 Dart 自身的构建系统以及 Pub 包管理器。这些工具将原生安卓和 iOS 包装应用的构建工作委托给各自的构建系统。
虽然你的 Flutter 项目的 android 文件夹下有 Gradle 文件,但只有在添加平台特定集成所需的原生依赖时才使用这些文件。
通常情况下,应使用 pubspec.yaml 来声明 Flutter 中要使用的外部依赖。查找 Flutter 包的一个好地方是 pub.dev。
Activities and fragments
Flutter 中与活动(activities)和碎片(fragments)相对应的是什么?
在安卓系统中,Activity(活动)代表用户可以执行的一个单一的聚焦操作。Fragment(碎片)则代表一种行为或用户界面的一部分。碎片是实现代码模块化、为更大屏幕构建复杂用户界面以及助力扩展应用程序界面的一种方式。在 Flutter 中,这两个概念都包含在 Widget(组件)的范畴内。
正如在 "意图" 部分所提到的,由于 Flutter 中万物皆为组件,所以 Flutter 中的屏幕由组件(Widgets)来表示。可以使用导航器(Navigator)在不同的路由(Routes)之间进行切换,这些路由代表着不同的屏幕或页面,也可能是相同数据的不同状态或不同渲染效果。
如何监听 Android 活动的生命周期事件?
在 Android 中,你可以重写 Activity 中的方法来捕获活动本身的生命周期方法,或者在 Application 上注册 ActivityLifecycleCallbacks。在 Flutter 中,这两个概念都不存在,但你可以通过挂钩到 WidgetsBinding 观察者并监听 didChangeAppLifecycleState () 变化事件来监听生命周期事件
可观察到的生命周期事件包括:
- detached 该应用程序仍托管在 Flutter 引擎上,但已与所有宿主视图分离
- inactive 该应用程序处于非活动状态,且不接收用户输入
- paused 该应用目前对用户不可见,不响应用户输入,且在后台运行。这相当于 Android 中的 onPause ()。
- resumed 该应用程序可见且正在响应用户输入。这相当于安卓系统中的 onPostResume () 方法。
正如你可能已经注意到的,只有一小部分 Activity 生命周期事件是可用的;虽然 FlutterActivity 在内部确实捕获了几乎所有的 Activity 生命周期事件,并将它们发送到 Flutter 引擎,但这些事件大多对你是隐藏的。Flutter 会为你处理引擎的启动和停止,而且在大多数情况下,在 Flutter 端观察 Activity 生命周期几乎没有必要。无论如何,如果你需要通过观察生命周期来获取或释放任何原生资源,那么你或许应该在原生端进行操作。
以下是一个如何观察包含活动的生命周期状态的示例:
kotlin
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()));
}
Layouts
LinearLayout 的等效物是什么?
在 Android 中,LinearLayout 用于线性排列控件,既可以水平排列,也可以垂直排列。
在 Flutter 中,可使用 Row 或 Column 控件来实现相同的效果。
kotlin
@override
Widget build(BuildContext context) {
return const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
kotlin
@override
Widget build(BuildContext context) {
return const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Column One'),
Text('Column Two'),
Text('Column Three'),
Text('Column Four'),
],
);
}
如果你注意到这两个代码示例除了 "Row" 和 "Column" 组件外完全相同。Row 和 Column 可以互换使用,而子组件保持不变
RelativeLayout 的等效布局是什么?
RelativeLayout 会将你的小部件相对于彼此进行布局。在 Flutter 中,有几种方法可以实现相同的结果。
你可以通过组合使用 Column(列)、Row(行)和 Stack(堆叠)组件来实现 RelativeLayout(相对布局)的效果。你可以在组件的构造函数中指定规则,以确定子组件相对于父组件的布局方式。
下面是官方推荐的一篇问答贴


下面是答案内容

kotlin
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
brightness: Brightness.dark,
primaryColorBrightness: Brightness.dark,
),
home: new HomeScreen(),
debugShowCheckedModeBanner: false,
);
}
}
class Song extends StatelessWidget {
const Song({ this.title, this.author, this.likes });
final String title;
final String author;
final int likes;
@override
Widget build(BuildContext context) {
TextTheme textTheme = Theme
.of(context)
.textTheme;
return new Container(
margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
decoration: new BoxDecoration(
color: Colors.grey.shade200.withOpacity(0.3),
borderRadius: new BorderRadius.circular(5.0),
),
child: new IntrinsicHeight(
child: new Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Container(
margin: const EdgeInsets.only(top: 4.0, bottom: 4.0, right: 10.0),
child: new CircleAvatar(
backgroundImage: new NetworkImage(
'http://thecatapi.com/api/images/get?format=src'
'&size=small&type=jpg#${title.hashCode}'
),
radius: 20.0,
),
),
new Expanded(
child: new Container(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
new Text(title, style: textTheme.subhead),
new Text(author, style: textTheme.caption),
],
),
),
),
new Container(
margin: new EdgeInsets.symmetric(horizontal: 5.0),
child: new InkWell(
child: new Icon(Icons.play_arrow, size: 40.0),
onTap: () {
// TODO(implement)
},
),
),
new Container(
margin: new EdgeInsets.symmetric(horizontal: 5.0),
child: new InkWell(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new Icon(Icons.favorite, size: 25.0),
new Text('${likes ?? ''}'),
],
),
onTap: () {
// TODO(implement)
},
),
),
],
),
),
);
}
}
class Feed extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new ListView(
children: [
new Song(title: 'Trapadelic lobo', author: 'lillobobeats', likes: 4),
new Song(title: 'Different', author: 'younglowkey', likes: 23),
new Song(title: 'Future', author: 'younglowkey', likes: 2),
new Song(title: 'ASAP', author: 'tha_producer808', likes: 13),
new Song(title: '🌲🌲🌲', author: 'TraphousePeyton'),
new Song(title: 'Something sweet...', author: '6ryan'),
new Song(title: 'Sharpie', author: 'Fergie_6'),
],
);
}
}
class CustomTabBar extends AnimatedWidget implements PreferredSizeWidget {
CustomTabBar({ this.pageController, this.pageNames })
: super(listenable: pageController);
final PageController pageController;
final List<String> pageNames;
@override
final Size preferredSize = new Size(0.0, 40.0);
@override
Widget build(BuildContext context) {
TextTheme textTheme = Theme
.of(context)
.textTheme;
return new Container(
height: 40.0,
margin: const EdgeInsets.all(10.0),
padding: const EdgeInsets.symmetric(horizontal: 20.0),
decoration: new BoxDecoration(
color: Colors.grey.shade800.withOpacity(0.5),
borderRadius: new BorderRadius.circular(20.0),
),
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: new List.generate(pageNames.length, (int index) {
return new InkWell(
child: new Text(
pageNames[index],
style: textTheme.subhead.copyWith(
color: Colors.white.withOpacity(
index == pageController.page ? 1.0 : 0.2,
),
)
),
onTap: () {
pageController.animateToPage(
index,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 300),
);
}
);
})
.toList(),
),
);
}
}
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => new _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
PageController _pageController = new PageController(initialPage: 2);
@override
build(BuildContext context) {
final Map<String, Widget> pages = <String, Widget>{
'My Music': new Center(
child: new Text('My Music not implemented'),
),
'Shared': new Center(
child: new Text('Shared not implemented'),
),
'Feed': new Feed(),
};
TextTheme textTheme = Theme
.of(context)
.textTheme;
return new Stack(
children: [
new Container(
decoration: new BoxDecoration(
gradient: new LinearGradient(
begin: FractionalOffset.topCenter,
end: FractionalOffset.bottomCenter,
colors: [
const Color.fromARGB(255, 253, 72, 72),
const Color.fromARGB(255, 87, 97, 249),
],
stops: [0.0, 1.0],
)
),
child: new Align(
alignment: FractionalOffset.bottomCenter,
child: new Container(
padding: const EdgeInsets.all(10.0),
child: new Text(
'T I Z E',
style: textTheme.headline.copyWith(
color: Colors.grey.shade800.withOpacity(0.8),
fontWeight: FontWeight.bold,
),
),
)
)
),
new Scaffold(
backgroundColor: const Color(0x00000000),
appBar: new AppBar(
backgroundColor: const Color(0x00000000),
elevation: 0.0,
leading: new Center(
child: new ClipOval(
child: new Image.network(
'http://i.imgur.com/TtNPTe0.jpg',
),
),
),
actions: [
new IconButton(
icon: new Icon(Icons.add),
onPressed: () {
// TODO: implement
},
),
],
title: const Text('tofu\'s songs'),
bottom: new CustomTabBar(
pageController: _pageController,
pageNames: pages.keys.toList(),
),
),
body: new PageView(
controller: _pageController,
children: pages.values.toList(),
),
),
],
);
}
}
ScrollView 的等效物是什么?
在安卓系统中,使用 ScrollView 来布局你的小部件 ------ 如果用户设备的屏幕比你的内容小,内容就会滚动。
在 Flutter 中,实现这一功能最简单的方法是使用 ListView 组件。对于从 Android 开发转过来的人来说,这可能看起来有些多余,但在 Flutter 中,ListView 组件既可以当作滚动视图(ScrollView),又能当作 Android 中的 ListView 来使用。
kotlin
@override
Widget build(BuildContext context) {
return ListView(
children: const <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
在 Flutter 中如何处理横屏过渡?
如果 AndroidManifest.xml 包含以下内容,FlutterView 会处理配置变更:
kotlin
android:configChanges="orientation|screenSize"