发布于 2025-12-08 0 阅读
0

使用 Flutter 构建 Twitter 布局

使用 Flutter 构建 Twitter 布局

Flutter 是谷歌推出的全新移动开发框架,它于去年首次发布,本月发布了 1.0 版本。我了解 Flutter 已有几个月,但自从它发布 1.0 版本以来,我决定深入研究它。我之前使用 Java 开发过原生 Android 应用,也使用 Xamarim 和 NativeScript 开发过跨平台应用,虽然不算什么,但我觉得 Flutter 是一个很有吸引力的领域,有些人甚至认为它是不远的未来。

演示完毕,现在让我们来看看精彩的细节。在我看来,Flutter 最引人注目的特性在于其布局机制的易懂性。基本上,一切都是 Widget,你可以将 Widget 放置在任何你想要的地方。Widget 的描述性非常强,其属性几乎涵盖了所有常见用法,因此构建布局非常轻松便捷。由于一切都是 Widget,因此该框架非常宽松、动态且逻辑性强,你无需担心出错,只需照做即可。而且,借助眨眼间即可实现的热重载,你可以轻松测试所有内容。

我对 Flutter 的体验还处于业余水平,我学习了Flutter 官网上最基础的教程,也阅读了一些关于 Flutter 布局处理的内容,尤其是这篇很棒的文章,它总结了 Flutter 布局结构的所有知识。我之前从未敢于尝试在以上任何框架中复制 Twitter 的布局,但有了 Flutter,我有足够的信心去做,因为我无所畏惧。

因此我们有这样的事情:

Twitter布局

为了简化布局,我只考虑了简单的推文,也就是说只包含文本,没有图片、引用等等。这只是个开始。不过,其余部分都会保留,包括顶部的应用栏、用于发推文的浮动按钮以及带有标签的底部栏。看起来太复杂了吗?分而治之才是最好的方法!

首先,让我们看看顶部的应用栏。我们可以看到一个圆形头像和一行写着“Home”的文字。应用栏是 Android 应用中非常常见的设计模式,Flutter 也深谙此道,因此 Flutter 提供了一些 Widget 来简化应用栏的构建。为此,我们使用一个ScaffoldWidget,它带有一个名为 的参数,appBar该参数接受一个AppBarWidget,如下所示:



return MaterialApp(
      title: "Twitter Layout",
      theme: new ThemeData(
        primaryColor: Color.fromRGBO(21, 32, 43, 1.0),
        accentColor: Colors.blueAccent[200],
      ),
      home: new Scaffold(
          appBar: new AppBar(
            title: Row(
              children: <Widget>[
                Container(
                  child: CircleAvatar(
                    child: new Text("L"),
                    radius: 15.0,
                  ),
                  margin: EdgeInsets.only(right: 30.0),
                ),
                new Text("Home")
              ],
            ),
            elevation: 4.0,
          ),
          body: _buildBody(context)
      ),
    );


Enter fullscreen mode Exit fullscreen mode

如您所见,该Scaffold小部件被一个小部件包裹,MaterialApp这使得使用Material Design(谷歌为 Android 应用设计的标准)构建应用程序变得简单,Twitter 应用程序显然也使用了该标准。该AppBar小部件有一个title参数,可以接受任何小部件,尽管通常它只是一个Text小部件。因此,我创建了一个Row小部件,负责将其子项水平对齐,这正是我们在本例中想要的方式。我们将小Row部件的子项添加到一个children参数中。在本例中,该小部件包含一个ContainerText作为子项。该Container小部件非常方便,因为它允许您为其子项自定义间距、定位和绘制配置。在本例中,我添加了一个CircleAvatar小部件作为其子项,并在圆圈右侧添加了 30 像素的边距,以便与“Home”文本分隔开来。小CircleAvatar部件的子项可以是任何小部件,但我们通常使用文本或图像,为了简单起见,我使用了Text带有字母“L”的 。半径表示圆圈的大小。如果您注意到,应用栏上的圆圈比推文上的圆圈略小,所以我必须对其进行配置。小部件parameter上的AppBar表示应用栏的高度,也就是它在其下方显示阴影的位置。最终,无需从头开始创建应用栏、圆形头像以及所有间距配置,Flutter 可以通过使用常见模式轻松实现所需的效果。小Scaffold部件还有一个body参数,在本例中,所有应用内容都位于该参数中。让我们开始吧。

_buildBody()为了组织布局,我按照如下方法制定了健身逻辑:



Widget _buildBody(context){
    return new Container(
      child: new Column(
        children: <Widget>[
          new Flexible(
              child: Scaffold(
                body: new ListView.builder(
                  itemBuilder: (_, int index) => _tweets[index],
                  itemCount: _tweets.length,
                  reverse: false,
                ),
                floatingActionButton: FloatingActionButton(
                    onPressed: null, child: Icon(Icons.edit)),
              )),
          new Container(
            decoration:
            new BoxDecoration(color: Theme.of(context).cardColor),
            child: _buildTabsBar(context),
          ),
        ],
      ),
    );
  }


Enter fullscreen mode Exit fullscreen mode

返回_buildBody()a Widget,在本例中为 a Container。容器包含一个Column子窗口小部件,所有神奇的事情都发生在此。在上面用作基础的图像中,我们可以看到推文列表,其正下方是标签栏,旁边是浮动按钮。窗口小部件的子窗口小Column部件的放置方式与它们在屏幕上的显示方式相同。首先,我们有一个Flexible窗口小部件,顾名思义,它是灵活的,这意味着在本例中它将占据父窗口小部件的剩余空间,这就是我们的列表和浮动按钮所在的位置。Flexible窗口小部件的子窗口小部件是一个Scaffold窗口小部件。没有人说它只能像我们以前使用的那样用作应用程序的主窗口小部件,尽管这样做很常见。它在这里用于方便创建浮动按钮,因为它具有一个floatingActionButton属性,该属性将FloatingActionButton带有编辑图标的窗口小部件作为子窗口小部件。它还有一个用于onPressed配置事件的属性,但由于这里的目标只是进行布局,所以null目前仅此而已。只有这样才能使浮动按钮准确地出现在我们想要的位置。和之前一样,在小部件body的参数中Scaffold,我们有一个主要内容,即推文列表,它是使用 实现的ListView,这是一种特殊的列,当内容无法完全容纳在小部件中时,可以滚动显示。它只是根据推文列表构建列表,我们稍后会看到。

现在,我们先来关注 的另一个子级Column,即包含 方法的容器_buildTabsBar()。让我们看看标签页是如何构成的。



Widget _buildTabsBar(context) {
    return Container(
      height: 60,
      color: Color.fromRGBO(21, 32, 43, 1.0),
      child: new Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: <Widget>[
          Icon(Icons.home, color: Theme.of(context).accentColor),
          Icon(Icons.search, color: Colors.grey[100]),
          Icon(Icons.notifications_none, color: Colors.grey[100]),
          Icon(Icons.mail_outline, color: Colors.grey[100])
        ],
      ),
    );
  }


Enter fullscreen mode Exit fullscreen mode

制作这个栏非常简单,而这正是它如此出色的原因。它只是一个Container带有Row图标作为子元素的容器,仅此而已。我配置了容器的高度和颜色。然后,Row我使用mainAxisAlignment属性使图标之间的空间均匀分布,使其看起来像原始布局。我还记得用 HTML 和 CSS 居中是多么痛苦,更不用说让空间均匀分布了。

那么,让我们看看最后一部分,推文。它看起来需要处理的元素更多,确实如此,但如果将其可视化为行和列的集合,它并不比其他布局部分复杂多少。为了使推文可重用且易于使用,ListView我创建了一个Tweet包含推文所有布局的类:



class Tweet extends StatelessWidget {

  Tweet({this.user, this.userHandle, this.text});

  final String user;
  final String userHandle;
  final String text;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(10.0),
      decoration: BoxDecoration(
          color: Color.fromRGBO(21, 32, 43, 1.0),
          border: Border(bottom: BorderSide())),
      child: new Row(
        mainAxisAlignment: MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          CircleAvatar(
            child: Text(user.substring(0, 1)),
          ),
          _tweetContent()
        ],
      ),
    );
  }

...

}


Enter fullscreen mode Exit fullscreen mode

简单来说,它是无状态的,仅获取用户、用户句柄和推文文本来构建推文。完整的推文可能是有状态的,因为它包含点赞、转发和回复的计数,并且包含更多参数。该build()方法返回一个容器,其中包含基本布局配置、填充和底部边框(用作推文的分隔线),然后它有一个Rowas 子节点。行包含发推文用户的圆形头像和推文内容,这些内容是通过一个方法构建的,以便于组织。mainAxisAlignment设置crossAxisAlignment为 asstart非常重要,因为它可以根据需要将圆形头像放置在左上角。

当然,该_tweetContent()方法会返回推文的内容。下面是完整的方法,因为查看所有元素更容易理解,之后我会解释代码。



Widget _tweetContent(){
    return Flexible(
      child: Container(
        margin: EdgeInsets.only(left: 10.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            Row(
              children: <Widget>[
                Text(user,
                    style: TextStyle(
                        color: Colors.white,
                        fontWeight: FontWeight.bold)),
                Container(
                  margin: EdgeInsets.only(left: 5.0),
                  child: Text(userHandle + " · 30m",
                      style: TextStyle(color: Colors.grey[400])),
                )
              ],
            ),
            Container(
                margin: EdgeInsets.only(top: 5.0),
                child: Text(text, style: TextStyle(color: Colors.white))),
            Container(
              margin: EdgeInsets.only(top: 10.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: <Widget>[
                  Row(
                    children: <Widget>[
                      Icon(Icons.message, color: Colors.white),
                      Container(
                        margin: EdgeInsets.only(left: 3.0),
                        child: Text("15",
                            style: TextStyle(color: Colors.white)),
                      )
                    ],
                  ),
                  Row(
                    children: <Widget>[
                      Icon(Icons.repeat, color: Colors.white),
                      Container(
                        margin: EdgeInsets.only(left: 3.0),
                        child: Text("15",
                            style: TextStyle(color: Colors.white)),
                      )
                    ],
                  ),
                  Row(
                    children: <Widget>[
                      Icon(Icons.favorite_border, color: Colors.white),
                      Container(
                        margin: EdgeInsets.only(left: 3.0),
                        child: Text("15",
                            style: TextStyle(color: Colors.white)),
                      )
                    ],
                  ),
                  Icon(Icons.share, color: Colors.white)
                ],
              ),
            )
          ],
        ),
      ),
    );
  }


Enter fullscreen mode Exit fullscreen mode

父元素是一个小部件,它允许元素占据剩余的可用空间,即上面方法Flexible创建的行空间。然后,我们有一个,它允许配置内容左侧所需的边距,以将其与圆形头像分开。最后,我们有一个,有趣的事情从这里开始。buildContainerColumn

我们再次使用crossAxisAlignmentmainAxisAlignmentstart将内容定位到左上角。列的第一个子项是 ,Row其中包含用户名、用户名和推文时间。为了使用户名与用户名略微分开,我们将 包裹Text在 中,Container就像我们之前做的那样。列的第二个子项是 ,Container它包裹推文的实际文本,并在顶部留出边距。最后,我们还有另一个带有 的容器作为Row子项来构建回复、转发、点赞和分享按钮。mainAxisAlignment设置为spaceBetween,以便所有图标之间都有相等的间距,并均匀分布在行宽上。非常方便的配置、定位和间距在其他框架中对我来说一直是非常麻烦的事情。每个图标Row本身都是一个,包含图标以及旁边的回复、转发等数量。很简单,不是吗?

让我们看看下面的最终结果:

使用 Flutter 制作的 Twitter 布局

除了图标可能是 Twitter 独有的之外,其他都差不多。推文只是一些示例。

我希望这篇文章能对那些正在考虑了解 Flutter 并开始使用的人有所帮助。对我来说,在布局创建方面,我对 Flutter 一见倾心。除非它在交互性方面有所限制(我还需要进行一些尝试),我相信它很快就会流行起来!

鏂囩珷鏉ユ簮锛�https://dev.to/leoat12/building-twitter-layout-with-flutter-363g