要說我最喜歡的遊戲,那必須是英雄聯盟。太多太多的回憶!今天咱們一塊兒使用Flutter
來開發一款英雄資料卡。上圖是APP的部分截圖,APP的總體設計看上去仍是很清爽的。首頁使用Tab展現英雄的六大分類,點擊英雄的條目會跳轉到英雄的詳情頁面。前端
- lib
- models
- utils
- views
- widgets
- main.dart
複製代碼
咱們先從項目的目錄結構講起吧,對APP來個總體上的把握。本APP咱們採用的目錄結構是很常見的,不單單是Flutter開發,如今的前端開發模式也基本類似:node
models
來定義數據模型utils
裏放一些公用的函數、接口、路由、常量等views
裏放的是頁面級別的組件widgets
裏放的是頁面中須要使用的小組件main.dart
是APP的啓動文件APP一定從main.dart
開始,一些模板化的代碼就不提了,有一點須要注意的是,APP狀態欄的背景是透明的,這個配置在main()
函數中:android
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%來自官網哦。另外本人服務器配置不是很高也不穩定,因此接口只供學習使用哦git
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
類:github
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一般須要三個步驟:api
Step1: 生成簽名sass
Step2: 對項目進行簽名配置bash
Step3: 打包服務器
在打包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]
複製代碼
在項目中新建文件<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
}
}
複製代碼
執行打包命令:
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