Android学Flutter学习笔记 第二节 Android视角认知Flutter(resource,生命周期,layout)

序言

上篇文章中,我们了解到了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"
相关推荐
KaiGer6662 小时前
AUTOSAR 学习效率翻倍:我如何把 CP/AP 规范重构成认知地图
学习
zh_xuan2 小时前
kotlin的常见空检查
android·开发语言·kotlin
科技林总3 小时前
【系统分析师】1.1 信息与信息系统
学习
HyperAI超神经8 小时前
在线教程丨 David Baker 团队开源 RFdiffusion3,实现全原子蛋白质设计的生成式突破
人工智能·深度学习·学习·机器学习·ai·cpu·gpu
YJlio11 小时前
VolumeID 学习笔记(13.10):卷序列号修改与资产标识管理实战
windows·笔记·学习
小龙11 小时前
【学习笔记】多标签交叉熵损失的原理
笔记·学习·多标签交叉熵损失
踏雪羽翼11 小时前
android TextView实现文字字符不同方向显示
android·自定义view·textview方向·文字方向·textview文字显示方向·文字旋转·textview文字旋转
lxysbly12 小时前
安卓玩MRP冒泡游戏:模拟器下载与使用方法
android·游戏
知识分享小能手12 小时前
Ubuntu入门学习教程,从入门到精通,Ubuntu 22.04的Linux网络配置(14)
linux·学习·ubuntu