本系列同步發表在 個人部落格,歡迎大家關注~
好久沒從 UI 設計圖來分解該怎麼轉換成程式碼了。
今天就看個分解圖吧~

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(),
          ],
        ),
      ),
    );
  }
}
再來看看程式碼是不是清晰多了呢?
原則上, TrendingPage 與 MainPage 差不多,我幾乎是同一套排版直接拿過來用的。
原設計圖上是分成 Repos 和 Users,不過之後我用的 API 會是使用 github-trending-api。
github-trending-api 中的分類是 Projects 和 Developers,所以我乾脆改成 TrendingProjects 和 TrendingDevelopers 囉~

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 這個函數來實現,讓它能拿成好幾個頭像並列。
小提醒:
- 說個小插曲,其實一開始我在
 List.trailing裡,用Row來排列兩個Text時有噴出錯誤,不過後來參考 StackOverflow - Placing two trailing icons in ListTile 解決。trendProjectList裡面的資料是暫時從github-trending-apiREADME.md 中借來的。- 顯示語言顏色標籤,由於 Flutter 不能直接吃
 #001122這種色碼字串,於是找來 StackOverflow - Flutter/Dart: Convert HEX color string to Color? 作支援~
沒錯,今天的套路就是先放分解圖~

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,不過目前看起來還沒有必要性,所以就先這樣吧~
--
成果
點選 GIF 可直接看 Commit
感覺起來好像沒作很多操作,但其實了很多時間調整怎麼用 Widget 到最大效果~
也是挺燒腦的... ![]()
30 天中,UI 部份也終於要到尾聲了~
如果看拉 UI 有些煩的同學,加油! 再撐個 2-3 天囉~