iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 10
0
Mobile Development

用 Flutter 開發一個 Android App 吧系列 第 10

用 Flutter 開發一個 Android App 吧 - Day 10. 趨勢頁面

本系列同步發表在 個人部落格,歡迎大家關注~

趨勢頁面(Trending Page)

好久沒從 UI 設計圖來分解該怎麼轉換成程式碼了。

今天就看個分解圖吧~

day10-1.png

lib/pages/trending/trending.dart

import 'package:flutter/material.dart';
import 'package:gitme_reborn/pages/trending/developer.dart';
import 'package:gitme_reborn/pages/trending/project.dart';

class TrendingPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: TabBar(
            tabs: <Widget>[
              Tab(text: "Project"),
              Tab(text: "Developer"),
            ],
          ),
          actions: <Widget>[
            PopupMenuButton(
              itemBuilder: (BuildContext context) {
                return [
                  PopupMenuItem(
                    child: Text("Date range: daily"),
                  ),
                ];
              },
            ),
          ],
        ),
        body: TabBarView(
          children: <Widget>[
            TrendingProjects(),
            TrendingDevelopers(),
          ],
        ),
      ),
    );
  }
}

再來看看程式碼是不是清晰多了呢?
原則上, TrendingPageMainPage 差不多,我幾乎是同一套排版直接拿過來用的。

原設計圖上是分成 Repos 和 Users,不過之後我用的 API 會是使用 github-trending-api

github-trending-api 中的分類是 Projects 和 Developers,所以我乾脆改成 TrendingProjectsTrendingDevelopers 囉~

趨勢專案(TrendingProjects)

day10-2.png

lib/pages/trending/project.dart

import "package:flutter/material.dart";
import 'package:gitme_reborn/utils.dart';

class TrendingProjects extends StatefulWidget {
  @override
  _TrendingProjectsState createState() => _TrendingProjectsState();
}

class _TrendingProjectsState extends State<TrendingProjects> {
  List trendProjectList = [
    {
      "author": "google",
      "name": "gvisor",
      "avatar": "https://github.com/google.png",
      "url": "https://github.com/google/gvisor",
      "description": "Container Runtime Sandbox",
      "language": "Go",
      "languageColor": "#3572A5",
      "stars": 3320,
      "forks": 118,
      "currentPeriodStars": 1624,
      "builtBy": [
        {
          "href": "https://github.com/viatsko",
          "avatar": "https://avatars0.githubusercontent.com/u/376065",
          "username": "viatsko"
        }
      ]
    },
    ...(略)
  ];

  @override
  Widget build(BuildContext context) {
    return Scrollbar(
      child: RefreshIndicator(
        child: ListView.separated(
          padding: EdgeInsets.all(0.0),
          itemCount: trendProjectList.length,
          itemBuilder: (BuildContext context, int index) {
            return ListTile(
              title: Text(
                  "${trendProjectList[index]["author"]} / ${trendProjectList[index]["name"]}"),
              subtitle: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  SizedBox(height: 8.0),
                  Text(
                      "★ ${trendProjectList[index]["currentPeriodStars"]} stars today"),
                  SizedBox(height: 8.0),
                  Text(trendProjectList[index]["description"]),
                  SizedBox(height: 8.0),
                  Row(
                      children: <Widget>[
                        Text("★ ${trendProjectList[index]["stars"]}"),
                        SizedBox(width: 16.0),
                        ...buildBuiltByList(trendProjectList[index]["builtBy"]),
                      ],
                    )
                ],
              ),
              trailing: Row(
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  Text("● ", style: TextStyle(color: hexToColor(trendProjectList[index]["languageColor"]), fontSize: 24.0),),
                  Text(trendProjectList[index]["language"]),
                ],
              ),
              contentPadding:
                  EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
              onTap: () {},
            );
          },
          separatorBuilder: (BuildContext context, int index) =>
              const Divider(height: 0.0),
        ),
        onRefresh: () async {
          return Future.delayed(Duration(seconds: 2), () {});
        },
      ),
    );
  }

  List<Padding> buildBuiltByList(List builtByList) {
    List builtBys = builtByList.map((builtBy) {
      return Padding(
        padding: const EdgeInsets.symmetric(horizontal: 2.0),
        child: CircleAvatar(
          radius: 10.0,
          backgroundImage: NetworkImage(builtBy["avatar"]),
        ),
      );
    }).toList();
    if (builtBys.length > 7) {
      return builtBys.sublist(0, 6);
    } else {
      return builtBys;
    }
  }
}

如果對照的分析圖看,ListTile 的部份應該沒什麼問題。

不過值得一提得是 CircleAvatar(s),這部份是自己寫了 buildBuiltByList 這個函數來實現,讓它能拿成好幾個頭像並列。

小提醒:

趨勢開發者(TrendingDevelopers)

沒錯,今天的套路就是先放分解圖~

day10-3.png

lib/pages/trending/developer.dart

import "package:flutter/material.dart";

class TrendingDevelopers extends StatefulWidget {
  @override
  _TrendingDevelopersState createState() => _TrendingDevelopersState();
}

class _TrendingDevelopersState extends State<TrendingDevelopers> {
  List trendDeveloperList = [
    {
      "username": "google",
      "name": "Google",
      "type": "organization",
      "url": "https://github.com/google",
      "avatar": "https://avatars0.githubusercontent.com/u/1342004",
      "repo": {
        "name": "traceur-compiler",
        "description":
            "Traceur is a JavaScript.next-to-JavaScript-of-today compiler",
        "url": "https://github.com/google/traceur-compiler"
      }
    },
    ...(略)
  ];

  @override
  Widget build(BuildContext context) {
    return Scrollbar(
      child: RefreshIndicator(
        child: ListView.separated(
          padding: EdgeInsets.all(0.0),
          itemCount: trendDeveloperList.length,
          itemBuilder: (BuildContext context, int index) {
            return ListTile(
              leading: CircleAvatar(
                backgroundImage:
                    NetworkImage(trendDeveloperList[index]["avatar"]),
                radius: 28.0,
              ),
              title: Row(
                // crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text(trendDeveloperList[index]["name"]),
                  SizedBox(width: 16.0),
                  Text(trendDeveloperList[index]["username"]),
                ],
              ),
              subtitle: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  SizedBox(height: 8.0),
                  Text(
                      "? ${trendDeveloperList[index]["repo"]["name"]}"),
                  SizedBox(height: 8.0),
                  Text(trendDeveloperList[index]["repo"]["description"]),
                ],
              ),
              contentPadding:
                  EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
              onTap: () {},
            );
          },
          separatorBuilder: (BuildContext context, int index) =>
              const Divider(height: 0.0),
        ),
        onRefresh: () async {
          return Future.delayed(Duration(seconds: 2), () {});
        },
      ),
    );
  }
}

理解了 TrendingPrjects,這個就簡單多了,只需要注意 ListTile 的屬性填入了什麼就好~

小提醒:

  • 如果想作的極致點,想再縮短一點程式碼,還可以將 ListTile 個別封裝成 Widget,不過目前看起來還沒有必要性,所以就先這樣吧~

--

成果

day10-4.gif

點選 GIF 可直接看 Commit

感覺起來好像沒作很多操作,但其實了很多時間調整怎麼用 Widget 到最大效果~
也是挺燒腦的... /images/emoticon/emoticon06.gif

30 天中,UI 部份也終於要到尾聲了~
如果看拉 UI 有些煩的同學,加油! 再撐個 2-3 天囉~


上一篇
用 Flutter 開發一個 Android App 吧 - Day 9. 個人頁面(續)
下一篇
用 Flutter 開發一個 Android App 吧 - Day 11. 設定頁面
系列文
用 Flutter 開發一個 Android App 吧30

尚未有邦友留言

立即登入留言