Flutter 多頁面電子書 App

Chen Chen
海大 SwiftUI iOS / Flutter App 程式設計
17 min readApr 10, 2024

Github網址

製作包含多個頁面的電子書 App,每個頁面定義對應的 StatelessWidget。

  • 定義資料的型別,比方電影 App 定義 Movie 型別。

chiikawa.dart

class Chiikawa {
final String name;
final String image;
final String text;
final String birthday;
const Chiikawa(
{required this.name,
required this.image,
required this.text,
required this.birthday});
factory Chiikawa.fromJson(Map<String, dynamic> json) {
return Chiikawa(
name: json['name'],
image: json['Image'],
text: json['description'],
birthday: json['birthday'],
);
}
}
  • 自訂 StatelessWidget 顯示 ListView 的資料,比方用 BookTile 顯示 Book 的內容。

story_tile.dart 顯示故事資料

import 'package:ebook/story.dart';
import 'package:flutter/material.dart';
import 'package:ebook/story_detail_tile.dart';

class StoryDetailPageTile extends StatelessWidget {
final Story story;
const StoryDetailPageTile({Key? key, required this.story}) : super(key: key);

@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StoryDetailTile(story: story),
),
);
},
child: Container(
child: Card(
color: Colors.grey[300],
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [Text(story.topic)],
),
),
),
),
);
}
}

chiikawa_tile.dart 顯示chiikawa資料

import 'package:ebook/chiikawa.dart';
import 'package:flutter/material.dart';
import 'package:ebook/chiikawa_detail_tile.dart';

class ChiikawaTile extends StatelessWidget {
final Chiikawa chiikawa;
const ChiikawaTile({Key? key, required this.chiikawa}) : super(key: key);

@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChiikawaDetailTile(chiikawa: chiikawa),
),
);
},
child: Container(
color: Colors.white,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/${chiikawa.image}.png',
width: 200,
height: 200,
),
Text(chiikawa.name),
],
),
),
);
}
}
  • 使用 Navigator 切換頁面、使用 GestureDetector、InkWell 或 TextButton 偵測點擊。
import 'package:ebook/chiikawa.dart';
import 'package:flutter/material.dart';
import 'package:ebook/chiikawa_detail_tile.dart';

class ChiikawaTile extends StatelessWidget {
final Chiikawa chiikawa;
const ChiikawaTile({Key? key, required this.chiikawa}) : super(key: key);

@override
Widget build(BuildContext context) {
return GestureDetector( //使用 GestureDetector、InkWell 或 TextButton 偵測點擊。
onTap: () {
Navigator.push( //使用 Navigator 切換頁面。
context,
MaterialPageRoute(
builder: (context) => ChiikawaDetailTile(chiikawa: chiikawa),
),
);
},
child: Container(
color: Colors.white,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/${chiikawa.image}.png',
width: 200,
height: 200,
),
Text(chiikawa.name),
],
),
),
);
}
}
  • 使用到 ListView.builder 或 ListView.separated、使用 GridView 製作格子狀排列的畫面。

main.dart

import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
import 'package:ebook/chiikawa_tile.dart';
import 'package:ebook/chiikawa.dart';
import 'package:flutter/material.dart';
import 'package:ebook/story.dart';
import 'package:ebook/story_tile.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}

class MyHomePage extends StatelessWidget {
Future<List<Chiikawa>> loadChiikawas() async {
String jsonString = await rootBundle.loadString('chiikawas.json');
final List<dynamic> jsonResponse = json.decode(jsonString);
return jsonResponse.map((item) => Chiikawa.fromJson(item)).toList();
}

Future<List<Story>> loadStory() async {
String jsonString = await rootBundle.loadString('story.json');
final List<dynamic> jsonResponse = json.decode(jsonString);
return jsonResponse.map((item) => Story.fromJson(item)).toList();
}

@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2, // 標籤的數量
child: Scaffold(
backgroundColor: Color.fromARGB(242, 242, 242, 242),
appBar: AppBar(
title: const Text('Chiikawa Home Page'),
bottom: TabBar(
tabs: <Widget>[
Tab(icon: Icon(Icons.view_module)),
Tab(icon: Icon(Icons.view_list)),
],
),
),
body: TabBarView(
children: [
FutureBuilder<List<Chiikawa>>(
future: loadChiikawas(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
return GridView.count( //使用 GridView 製作格子狀排列的畫面。
crossAxisCount: 2,
children: snapshot.data!
.map((chiikawa) => ChiikawaTile(chiikawa: chiikawa))
.toList(),
);
} else if (snapshot.hasError) {
return Text("Error: ${snapshot.error}");
}
}
return CircularProgressIndicator();
},
),
FutureBuilder<List<Story>>(
future: loadStory(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
return ListView.builder( //使用到 ListView.builder
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
final story = snapshot.data![index];
return StoryDetailPageTile(story: story);
},
);
} else if (snapshot.hasError) {
return Text("Error: ${snapshot.error}");
}
}
return CircularProgressIndicator();
},
),
],
),
),
);
}
}
  • 使用 DefaultTabController、TabBar、TabBarView 製作 tab 分頁。
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
import 'package:ebook/chiikawa_tile.dart';
import 'package:ebook/chiikawa.dart';
import 'package:flutter/material.dart';
import 'package:ebook/story.dart';
import 'package:ebook/story_tile.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}

class MyHomePage extends StatelessWidget {
Future<List<Chiikawa>> loadChiikawas() async {
String jsonString = await rootBundle.loadString('chiikawas.json');
final List<dynamic> jsonResponse = json.decode(jsonString);
return jsonResponse.map((item) => Chiikawa.fromJson(item)).toList();
}

Future<List<Story>> loadStory() async {
String jsonString = await rootBundle.loadString('story.json');
final List<dynamic> jsonResponse = json.decode(jsonString);
return jsonResponse.map((item) => Story.fromJson(item)).toList();
}

@override
Widget build(BuildContext context) {
return DefaultTabController( //【DefaultTabController】
length: 2, // 標籤的數量
child: Scaffold(
backgroundColor: Color.fromARGB(242, 242, 242, 242),
appBar: AppBar(
title: const Text('Chiikawa Home Page'),
bottom: TabBar( //【TabBar】
tabs: <Widget>[
Tab(icon: Icon(Icons.view_module)),
Tab(icon: Icon(Icons.view_list)),
],
),
),
body: TabBarView(
children: [
FutureBuilder<List<Chiikawa>>(
future: loadChiikawas(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
return GridView.count(
crossAxisCount: 2,
children: snapshot.data!
.map((chiikawa) => ChiikawaTile(chiikawa: chiikawa))
.toList(),
);
} else if (snapshot.hasError) {
return Text("Error: ${snapshot.error}");
}
}
return CircularProgressIndicator();
},
),
FutureBuilder<List<Story>>(
future: loadStory(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
final story = snapshot.data![index];
return StoryDetailPageTile(story: story);
},
);
} else if (snapshot.hasError) {
return Text("Error: ${snapshot.error}");
}
}
return CircularProgressIndicator();
},
),
],
),
),
);
}
}
  • 使用 Card widget。
import 'package:ebook/story.dart';
import 'package:flutter/material.dart';
import 'package:ebook/story_detail_tile.dart';

class StoryDetailPageTile extends StatelessWidget {
final Story story;
const StoryDetailPageTile({Key? key, required this.story}) : super(key: key);

@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StoryDetailTile(story: story),
),
);
},
child: Container(
child: Card( //使用 Card widget。
color: Colors.grey[300],
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [Text(story.topic)],
),
),
),
),
);
}
}

加分功能

  • 播放背景音樂。
import 'package:audioplayers/audioplayers.dart';

final AudioPlayer audioPlayer = AudioPlayer();

MyHomePage({super.key}) {
playBackgroundMusic();
}

void playBackgroundMusic() async {
audioPlayer.setReleaseMode(ReleaseMode.loop);

await audioPlayer.play(AssetSource('audio/chiikawa_bgm.mp3'));
}

--

--