Flutter之英雄聯盟

小華堅決上王者發表於2019-07-28

Flutter之英雄聯盟

要說我最喜歡的遊戲,那必須是英雄聯盟。太多太多的回憶!今天我們一起使用Flutter來開發一款英雄資料卡。上圖是APP的部分截圖,APP的整體設計看上去還是很清爽的。首頁使用Tab展示英雄的六大分類,點選英雄的條目會跳轉到英雄的詳情頁面。

目錄結構

- lib
    - models
    - utils
    - views
    - widgets
    - main.dart
複製程式碼

我們先從專案的目錄結構講起吧,對APP來個整體上的把握。本APP我們採用的目錄結構是很常見的,不僅僅是Flutter開發,現在的前端開發模式也基本相似:

  • models來定義資料模型
  • utils裡放一些公用的函式、介面、路由、常量等
  • views裡放的是頁面級別的元件
  • widgets裡放的是頁面中需要使用的小元件
  • main.dart 是APP的啟動檔案

開始之處

APP必定從main.dart開始,一些模板化的程式碼就不提了,有一點需要注意的是,APP狀態列的背景是透明的,這個配置在main()函式中:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(MyApp());
  if (Platform.isAndroid) {
    SystemUiOverlayStyle systemUiOverlayStyle =
        SystemUiOverlayStyle(statusBarColor: Colors.transparent);
    SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
  }
}
複製程式碼

首頁

APP進入首頁後開始拉取後端介面的資料,進而展示英雄列表。TabBar元件來定義頁面上部的Tab切換,TabBarView來展示頁面下部的列表。本來打算使用拳頭開放的介面資料,但是沒有提供中文翻譯。就去騰訊找了下,騰訊更加封閉,居然沒有開發者介面。無賴之舉,自己用node寫了個介面來提供實時的英雄資料,資料100%來自官網哦。另外本人伺服器配置不是很高也不穩定,所以介面只供學習使用哦

import 'package:flutter/material.dart';
import 'package:lol/views/homeList.dart';
import 'package:lol/utils/api.dart' as api;
import 'package:lol/utils/constant.dart';
import 'package:lol/utils/utils.dart';

class HomeView extends StatefulWidget {
  HomeView({Key key}) : super(key: key);

  _HomeViewState createState() => _HomeViewState();
}

class _HomeViewState extends State<HomeView> with SingleTickerProviderStateMixin {
  TabController _tabController;
  List<dynamic> heroList = [];

  @override
  void initState() {
    super.initState();
    _tabController = TabController(vsync: this, initialIndex: 0, length: 6);
    init();
  }

  init() async {
    Map res = await api.getHeroList();
    setState(() {
     heroList = res.values.toList();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: TabBar(
          controller: _tabController,
          tabs: <Widget>[
            Tab(text: '戰士'),
            Tab(text: '坦克'),
            Tab(text: '法師'),
            Tab(text: '刺客'),
            Tab(text: '輔助'),
            Tab(text: '射手'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: <Widget>[
          HomeList(data: Utils.filterHeroByTag(heroList, Tags.Fighter)),
          HomeList(data: Utils.filterHeroByTag(heroList, Tags.Tank)),
          HomeList(data: Utils.filterHeroByTag(heroList, Tags.Mage)),
          HomeList(data: Utils.filterHeroByTag(heroList, Tags.Assassin)),
          HomeList(data: Utils.filterHeroByTag(heroList, Tags.Support)),
          HomeList(data: Utils.filterHeroByTag(heroList, Tags.Marksman)),
        ],
      ),
    );
  }
}
複製程式碼

首頁列表

首頁的六個列表都是一樣的,只是資料不同,所以公用一個元件homeList.dart即可,切換Tab的時候為了不銷燬之前的頁面需要讓元件繼承AutomaticKeepAliveClientMixin類:

import 'package:flutter/material.dart';
import 'package:lol/widgets/home/heroItem.dart';
import 'package:lol/models/heroSimple.dart';

class HomeList extends StatefulWidget {
  final List data;
  HomeList({Key key, this.data}) : super(key: key);

  _HomeListState createState() => _HomeListState();
}

class _HomeListState extends State<HomeList>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Container(
      padding: EdgeInsets.symmetric(vertical: 5),
      child: ListView.builder(
        itemCount: widget.data.length,
        itemBuilder: (BuildContext context, int index) {
          return HeroItem(data: HeroSimple.fromJson(widget.data[index]));
        },
      ),
    );
  }
}
複製程式碼

英雄詳情

點選英雄條目,路由跳轉到詳情頁面heroDetail.dart,這個頁面中包含了很多小元件,其中的皮膚預覽功能使用的是第三方的圖片檢視庫extended_image,這個庫很強大,而且還是位中國開發者,必須支援。

import 'package:flutter/material.dart';
import 'package:lol/utils/api.dart' as api;
import 'package:lol/models/heroSimple.dart';
import 'package:lol/models/heroDetail.dart';
import 'package:lol/utils/utils.dart';
import 'package:lol/widgets/detail/detailItem.dart';
import 'package:lol/widgets/detail/skin.dart';
import 'package:lol/widgets/detail/info.dart';

class HeroDetail extends StatefulWidget {
  final HeroSimple heroSimple;
  HeroDetail({Key key, this.heroSimple}) : super(key: key);

  _HeroDetailState createState() => _HeroDetailState();
}

class _HeroDetailState extends State<HeroDetail> {
  HeroDetailModel _heroData; // hero資料
  bool _loading = false; // 載入狀態
  String _version = ''; // 國服版本
  String _updated = ''; // 文件更新時間

  @override
  void initState() {
    super.initState();
    init();
  }

  init() async {
    setState(() {
      _loading = true;
    });
    Map res = await api.getHeroDetail(widget.heroSimple.id);
    var data = res['data'];
    String version = res['version'];
    String updated = res['updated'];
    print(version);
    setState(() {
      _heroData = HeroDetailModel.fromJson(data);
      _version = version;
      _updated = updated;
      _loading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.heroSimple.name), elevation: 0),
      body: _loading
          ? Center(child: CircularProgressIndicator())
          : SingleChildScrollView(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  DetailItem(
                    title: '皮膚',
                    child: Skins(imgList: _heroData.skins),
                  ),
                  DetailItem(
                    title: '型別',
                    child: Row(
                        children: _heroData.tags
                            .map((tag) => Container(
                                  margin: EdgeInsets.only(right: 10),
                                  child: CircleAvatar(
                                    child: Text(
                                      Utils.heroTagsMap(tag),
                                      style: TextStyle(color: Colors.white),
                                    ),
                                  ),
                                ))
                            .toList()),
                  ),
                  DetailItem(
                    title: '屬性',
                    child: HeroInfo(data: _heroData.info),
                  ),
                  DetailItem(
                    title: '使用技巧',
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: _heroData.allytips
                          .map((tip) => Column(
                                children: <Widget>[
                                  Text(tip),
                                  SizedBox(height: 5)
                                ],
                              ))
                          .toList(),
                    ),
                  ),
                  DetailItem(
                    title: '對抗技巧',
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: _heroData.enemytips
                          .map((tip) => Column(
                                children: <Widget>[
                                  Text(tip),
                                  SizedBox(height: 5)
                                ],
                              ))
                          .toList(),
                    ),
                  ),
                  DetailItem(
                    title: '背景故事',
                    child: Text(_heroData.lore),
                  ),
                  DetailItem(
                    title: '國服版本',
                    child: Text(_version),
                  ),
                  DetailItem(
                    title: '更新時間',
                    child: Text(_updated),
                  )
                ],
              ),
            ),
    );
  }
}
複製程式碼

打包APK

打包APK通常需要三個步驟:

Step1: 生成簽名

Step2: 對專案進行簽名配置

Step3: 打包

Step1: 生成簽名

在打包APK之前需要生成一個簽名檔案,簽名檔案是APP的唯一標識:

keytool -genkey -v -keystore c:/Users/15897/Desktop/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
複製程式碼
  • c:/Users/15897/Desktop/key.jks表示檔案的生成位置,我直接設定的桌面
  • -validity 10000設定的簽名的有效時間
  • -alias key為簽名檔案起個別名,我直接設定成key

執行這條命令列後,會有一個互動式的問答:

輸入金鑰庫口令:
再次輸入新口令:
您的名字與姓氏是什麼?
  [Unknown]:  hua
您的組織單位名稱是什麼?
  [Unknown]:  xxx
您的組織名稱是什麼?
  [Unknown]:  xxx
您所在的城市或區域名稱是什麼?
  [Unknown]:  xxx
您所在的省/市/自治區名稱是什麼?
  [Unknown]:  xxx
該單位的雙字母國家/地區程式碼是什麼?
  [Unknown]:  xxx
CN=hua, OU=xxx, O=xxx, L=xxx, ST=xxx, C=xxx是否正確?
  [否]:  y

正在為以下物件生成 2,048 位RSA金鑰對和自簽名證書 (SHA256withRSA) (有效期為 10,000 天):
         CN=hua, OU=xxx, O=xxx, L=xxx, ST=xxx, C=xxx
輸入 <key> 的金鑰口令
        (如果和金鑰庫口令相同, 按回車):
[正在儲存c:/Users/15897/Desktop/key.jks]
複製程式碼

Step2: 對專案進行簽名配置

在專案中新建檔案<app dir>/android/key.properties,檔案中定義了四個變數,留著給<app dir>/android/app/build.gradle呼叫。

前三個都是上一步用到的幾個欄位,第四個storeFile是簽名檔案的位置,檔案位置是相對於<app dir>/android/app/build.gradle來說,所以需要將上一步生成了的key.jks複製到<app dir>/android/app/下。

警告:檔案涉及到密碼啥的,所以最好不要上傳到版本管理。

#  Note: Keep the key.properties file private; do not check it into public source control.
storePassword=123456
keyPassword=123456
keyAlias=key
storeFile=key.jks
複製程式碼

再修改<app dir>/android/app/build.gradle這裡才是正在的配置):

def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

android {

複製程式碼
signingConfigs {
    release {
        keyAlias keystoreProperties['keyAlias']
        keyPassword keystoreProperties['keyPassword']
        storeFile file(keystoreProperties['storeFile'])
        storePassword keystoreProperties['storePassword']
    }
}
buildTypes {
    release {
        signingConfig signingConfigs.release
    }
}
複製程式碼

Step3: 打包

執行打包命令:

flutter build apk
複製程式碼
You are building a fat APK that includes binaries for android-arm, android-arm64.
If you are deploying the app to the Play Store, it's recommended to use app bundles or split the APK to reduce the APK size.
    To generate an app bundle, run:
        flutter build appbundle --target-platform android-arm,android-arm64
        Learn more on: https://developer.android.com/guide/app-bundle
    To split the APKs per ABI, run:
        flutter build apk --target-platform android-arm,android-arm64 --split-per-abi
        Learn more on:  https://developer.android.com/studio/build/configure-apk-splits#configure-abi-split
Initializing gradle...                                              3.6s
Resolving dependencies...                                          26.8s
Calling mockable JAR artifact transform to create file: C:\Users\15897\.gradle\caches\transforms-1\files-1.1\android.jar\e122fbb402658e4e43e8b85a067823c3\android.jar with input C:\Users\15897\AppData\Local\Android\Sdk\platforms\android-28\android.jar
Running Gradle task 'assembleRelease'...
Running Gradle task 'assembleRelease'... Done                      84.7s
Built build\app\outputs\apk\release\app-release.apk (11.2MB).
複製程式碼

打包完成後,apk檔案就在這裡build\app\outputs\apk\release\app-release.apk

打包方面相關連結

更多連結

相關文章