如何用Flutter创建一个自适应应用程序(附代码)

311 阅读7分钟

Flutter已被广泛采用,因为它可以灵活地构建应用程序,用一个代码库在你的Android、iOS、macOS和Windows机器上运行。在大多数情况下,这些设备有不同的尺寸,尽管Flutter有跨平台的能力,但你不能保证你的应用程序的界面会在这些设备上默认呈现出预期的效果。这是你在开发过程中必须考虑的一个特点。

本文将演示如何用Flutter创建一个自适应应用程序。您将学习如何建立一个基本的电子商务应用程序,以适应特定设备的尺寸,而不必在屏幕尺寸变化时重建应用程序。我们将介绍:

什么是自适应应用程序?

当一个应用程序被设计成在运行时根据特定的参数改变其属性值以响应不同的输入或条件时,它就被认为是自适应的。例如,一个自适应应用程序使用相同的代码库在移动和桌面视图上渲染不同的界面。

值得注意的是,自适应设计不应该与响应式设计相混淆。响应式应用会根据可用的屏幕尺寸动态地改变其界面的布局,但保持布局的设计。应用程序的内容只是被重新排列以适应屏幕的大小。

另一方面,自适应设计要求你创建特定平台的设计、内容和模板。也就是说,你的应用程序在移动设备上的设计在更大的屏幕上(如桌面设备)会有所不同。

构建一个自适应应用程序时需要考虑什么

为了改善用户体验,你应该设计你的应用程序,以适应不同的设备(手机、折叠式设备、平板电脑、智能手表和个人电脑)的不同设计、尺寸、形状和操作系统。你的用户应该能够在这些不同的设备上探索你的应用程序,并仍然享受设备和操作系统的原生体验。

在设计或开发阶段,你必须做出的一个重要决定是确定你的应用程序应该切换其布局的阈值。在安卓文档的一个章节中,有一组推荐值,以帮助你决定你的应用程序在不同屏幕尺寸下的布局。

在大多数情况下,由于垂直滚动的影响,屏幕的可用宽度可能比其可用高度更重要。你还需要考虑到鼠标、键盘、触摸输入以及每个平台特有的其他特征。你应该定制你的应用程序的体验和界面,以适应主机设备。

使用FlutterLayoutBuilder 来创建一个自适应应用程序

Flutter提供了许多小工具,您可以在构建自适应应用程序时使用。还有一些外部包,如Flutter自适应用户界面自适应布局,您可以将其集成到您的应用程序中。在这篇文章中,我们将重点讨论使用Flutter提供的 [LayoutBuilder widget](https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html)来构建一个自适应应用程序。

LayoutBuilder 类对于构建响应式和自适应应用程序非常有用,因为它有一个在布局时调用的构建器函数。这个函数返回一个部件,接收应用上下文和约束条件,并根据约束条件的值执行具体操作。例如,当设备宽度满足通过constraints.maxWidth 属性指定的条件时,你可以选择呈现不同的设计:

LayoutBuilder(
        builder: (ctx, constraints) {
          if (constraints.maxWidth < 600) {
              return widget;
          }else{
            return anotherWidget;
          }
        }
    );

项目概述

本教程的演示应用程序是一个电子商务应用程序,它在一个可滚动的视图中显示其产品。在移动设备上,每个产品都占用了可用的宽度,用户可以垂直滚动屏幕来查看每个可用的产品。移动视图也将包含一个带有导航按钮的应用程序抽屉。

在更大的屏幕上,如桌面设备上,产品分布在屏幕的宽度和高度上。桌面视图也将在屏幕顶部包含一个导航栏,以取代移动视图中的应用抽屉。

如前所述,该应用程序将使用Flutter [LayoutBuilder class](https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html)来确保它在运行时根据可用的屏幕尺寸渲染指定的界面,而不必重建应用程序。

下面是应用程序在不同屏幕尺寸下的预期输出:

The expected output of our adaptive app example

先决条件

教程:使用 Flutter 创建一个自适应电子商务应用程序

设置

打开你的终端,运行以下命令来创建引导的Flutter项目,然后在你喜欢的IDE中打开该项目。

flutter create shop_app

lib 文件夹中,创建两个包--widgetsscreens --它们将分别包含您应用程序的部件和屏幕的Dart文件。

lib 文件夹中创建一个dummy_data.dart 文件,然后将本教程的GitHub repo中的内容复制到该文件中。你将在随后的章节中使用这个文件来生成Product widgets。

构建Product 小工具

Product 小组件代表了应用程序上每个产品项目的模板。在widgets 包中,创建一个product.dart 文件并添加以下代码:

import 'package:flutter/material.dart';

class Product extends StatelessWidget {
  final String id;
  final String name;
  final Color color;
  final String imageUrl;
  final double price;

  const Product({
    required this.id,
    required this.name,
    this.color = Colors.amberAccent,
    required this.imageUrl,
    required this.price
  });
}

这里是小部件的build 方法:

 @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () => (){},
      child: Card(
        shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(15)
        ),
        elevation: 4,
        margin: EdgeInsets.fromLTRB(15, 20, 15, 20),
        child: Column(
          children: [
            Expanded(
              child: Stack(
                children: [
                  ClipRRect(
                    borderRadius: const BorderRadius.only(
                        topLeft: Radius.circular(15),
                        topRight: Radius.circular(15)),
                    child: Image.network(
                      imageUrl,
                      height: 250,
                      width: double.infinity,
                      fit: BoxFit.cover,
                    ),
                  ),
                  Positioned(
                    bottom: 20,
                    right: 10,
                    child: Container(
                      color:  Colors.black54,
                      width: 300,
                      padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20),
                      child: Text(name,
                        style: const TextStyle(
                          fontSize: 18,
                          color: Colors.white,
                          fontWeight: FontWeight.bold
                        ),
                        softWrap: true,
                        overflow: TextOverflow.fade,
                      ),
                    ),
                  )
                ],
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(20.0),
              child:
                  Row(
                    children: [
                      const Icon(Icons.attach_money),
                      const SizedBox(width: 6),
                      Text('$price')
                    ],
                  ),
              ),
          ],
        ),
      ),
    );
  }

上面的widget使用了Flutter类和widget的组合,包括:

  • InkWell类来增强应用程序的用户体验
  • 堆栈(Stack),用于将小部件放在彼此的上面
  • 定位部件,将产品名称放置在一个特定的位置。

下面是Product widget的输出:

Output of our product widget

建立导航抽屉

导航抽屉将被专门用于应用程序的移动视图中;也就是说,当在移动设备上打开时,应用程序将呈现该抽屉。在widgets 包内,创建一个名为navigation_drawer 的Dart文件,并在该文件中包括以下代码:

import 'package:flutter/material.dart';

class NavigationDrawer extends StatelessWidget {
  const NavigationDrawer({Key? key}) : super(key: key);

  Widget buildListTile(String title, IconData icon){
    return ListTile(
      leading: Icon(icon,
        size: 26,),
      title: Text(
        title,
        style: const TextStyle(
            fontSize: 14,
            fontWeight: FontWeight.bold
        ),
      ),
      onTap: () {},
    );
  }

  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: Column(
        children: [
          Container(
            height: 100,
              padding: EdgeInsets.all(20),
          alignment: Alignment.bottomCenter,
            color: Theme.of(context).accentColor,
            child: Text('App Menu',
                style: TextStyle(
                  fontWeight: FontWeight.w900,
                  fontSize: 20,
                  color: Theme.of(context).primaryColor,
                ),
            ),
          ),
          const SizedBox(height: 20),
          buildListTile('Home', Icons.home),
          buildListTile('About', Icons.info_outline),
          buildListTile('Contact', Icons.contact_phone_outlined),
          buildListTile('Order', Icons.shopping_cart),
        ],
      ),
    );
  }
}

从上面的代码中,buildListTile 方法为抽屉里的特定项目建立了一个ListTile部件,在这个例子中,Home,About,Contact, 和Order

Our ListTile widget with the app drawer

渲染移动屏幕

接下来,创建屏幕来渲染应用程序的移动视图。在screens 包中,创建一个名为mobile_product_view 的Dart文件,并在该文件中包含以下代码片段:

import 'package:flutter/material.dart';
import 'package:shop_app/widgets/product.dart';

import '../dummy_data.dart';
import '../widgets/navigation_drawer.dart';

class MobileProductView extends StatelessWidget {
  final String title;

  const MobileProductView({Key? key, required this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
   return Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        drawer: NavigationDrawer(),
     body:  GridView.count(
       crossAxisCount: 1,
    children: DUMMY_DATA.map((product) =>
    Product(id: product.id, name: product.name, imageUrl: product.imageUrl, color: product.color, price: product.price)).toList(),
    )
    );
  }
}

上面的代码显示了应用程序使用来自dummy_data.dart 文件的数据来建立一个Product 小部件的列表,这些小部件被渲染成GridView

Our list of Product widgets in a GridView

渲染桌面屏幕

本节将包含应用程序的桌面视图的实现。在screens 包中创建一个Dart文件,名称为desktop_product_view 。在该文件中添加以下代码:

import 'package:flutter/material.dart';
import 'package:shop_app/widgets/product.dart';

import '../dummy_data.dart';

class DesktopProductView extends StatelessWidget {
  final String title;
  final int crossAxisCount;

  const DesktopProductView({Key? key, required this.title, required this.crossAxisCount}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {

    Widget getAction(String actionName){
      return TextButton(
        onPressed: () {},
        child: Text(
          actionName,
          style: TextStyle(
              color: Theme.of(context).accentColor,
            fontWeight: FontWeight.w500
          ),
        ),
      );
    }

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
        actions: [
          Padding(
            padding: const EdgeInsets.only(right: 100.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                getAction('Home'),
                getAction('About'),
                getAction('Contact'),
                getAction('Order'),
              ],
            ),
          )
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(18.0),
        child: GridView.count(
         crossAxisCount: crossAxisCount,
          children: DUMMY_DATA.map((product) =>
              Product(
                  id: product.id,
                  name: product.name,
                  imageUrl: product.imageUrl,
                  color: product.color,
                  price: product.price)
          ).toList(),
        ),
      ),
    );
  }
}

从上面的代码中,小部件通过其构造函数接收crossAxisCount 值。有了这个值,它就指定了Product widget的数量,以便在运行时呈现在GridView

AppBar 小组件中的动作将取代你为移动视图创建的抽屉。使用getAction 方法,你为AppBar 创建特定的动作部件:

Our AppBar widget replaces the navigation drawer

实现LayoutBuilder

LayoutBuilder 类的实现将在main.dart 文件中完成。这是因为我们要把应用程序的整个屏幕设计成自适应的。因此,LayoutBuilder 应该从最外层的小部件中建立接口,在这种情况下,它被放在main.dart 文件中。

下面是你应该添加到main.dart 文件中的代码:

import 'package:flutter/material.dart';
import 'package:shop_app/screens/mobile_product_view.dart';

import 'screens/desktop_product_view.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.purple,
        accentColor: Colors.amber
      ),
      home: const MyHomePage(title: 'Flutter adaptive app'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (ctx, constraints){
        if(constraints.maxWidth < 600){
          return MobileProductView(title: widget.title);
        }
        int crossAxisCount = 0;
        if(constraints.maxWidth > 1400) {
          crossAxisCount = 4;
        } else if(constraints.maxWidth > 900) {
          crossAxisCount = 3;
        } else if (constraints.maxWidth >= 600) {
          crossAxisCount = 2;
        }
        return DesktopProductView(title: widget.title, crossAxisCount: crossAxisCount);
      }
    );
  }
}

MyHomePage 小组件的build 方法根据以下约束条件,使用LayoutBuilder 构建屏幕:

  • 如果设备屏幕的宽度小于600像素,它将建立MobileProductView ,它代表你在上一节为移动设备创建的界面。
  • 如果设备屏幕的宽度大于600像素,它会建立DesktopProductView ,但会根据设备的宽度指定crossAxisCount ,如代码所示。

如果你打算只让你的应用程序的一个特定部分自适应,而不是像上面展示的那样让整个屏幕自适应,你就必须在小部件的所需部分实现LayoutBuilder

总结

让你的应用程序自适应不同的操作系统和设备屏幕尺寸是提高用户体验的一个好方法。本教程演示了如何使用FlutterLayoutBuilder 类使你的应用程序自适应。正如文章开头所提到的,在构建自适应应用程序时,你还可以采用其他选项,如自适应布局包

本教程中构建的项目可以在GitHub上找到。我还建议你探索Flutter文档,以更广泛地了解在你的应用程序的不同部分构建自适应布局的情况。