はじめに
アプリ開発のエンジニアがフロントエンド・バックエンド・インフラというように専業化されて、様々な開発言語やツールが生まれた現在において、バックエンドエンジニアが戦々恐々ながらにフロントエンドの開発に触れたらどこまでやれるのか、実践してみました。
技術選定および条件
今では数多くあるフロントエンド開発における術として、今回はFlutter
を選択しました。
Flutterとは・・・
Googleによって開発されたフリーかつオープンソースのUIのSDK。
単一のコードベースから、Android、iOS、Linux、macOS、Windows向けのクロスプラットフォームアプリケーションを開発することができる。
FlutterではDart言語を用いる。
DartはJavaScriptのような動的型付け言語でありながら、C#のような静的型付け言語の利点も兼ね備えたオープンソースのプログラミング言語。
また、今回は開発環境の構築から始めて、7時間程度でどれくらいまで作ることができるのか挑戦してみました。
ゴール目標
何でもよかったのですが、これを考えるのに時間を使っても仕方がないですし、万が一にも制限時間内に完成してしまうことがないように、先日買い物で使ったCHAT●RAISEアプリのホーム画面的なデザインのものを作ってみることにしました。
CH●TERAISEアプリのホーム画面がどんな姿をしているかは、是非お手元のスマートフォンで確認してみてください(宣伝)
開発環境準備(~1.0時間経過)
ほぼゼロから開発を始めるため、まずは開発環境の構築を始めます。
OSはWindows、エディターはvscodeを使います
(ネットの情報を見るに、OSはMac、エディターはAndroid Studioを使ったほうがいいのかもしれません)。
Flutter開発環境の構築手順として、こちらの通りに進めました。
細かい手順の記載は省略しますが、Android端末のエミュレータを動かすためにAndroid Studioを入れたり、設定変更のためにPC再起動したりと、結構時間がかかりましたね…。
Flutterチュートリアルの実施(~2.0時間経過)
vscodeの使い方以外何も知らないので、時間は惜しいですがcodelabのチュートリアルを丁寧に進めました。
「⑨次のステップ」のところまで進めました。雰囲気はつかめましたかね。
ホーム画面の大枠の作成(~3.0時間経過)
プロジェクト新規作成後、後で整理するのも大変そうだったので、ある程度必要になりそうな機能にアタリをつけて構成を作ります。
新規プロジェクト作成
まずは新規プロジェクトの立ち上げです。
Ctrl + Shift + P
を押してから、Flutter: New Project
でApplication
を選択し、プロジェクトを作ります。
ディレクトリ構成
ディレクトリ構成のうち、今回触れる部分だけを記載します。
新規作成するファイルはとりあえず空で用意だけします。
.
├── lib
│ └── main.dart ★変更
│ └── footer.dart ★新規作成
│ └── header.dart ★新規作成
├── pubspec.yaml
And More...
ヘッダの作成
当然奈良が難しいことは当然できないので、それっぽくなることだけ意識して作ります。
import 'package:flutter/material.dart';
class Header extends StatelessWidget implements PreferredSizeWidget {
const Header({Key? keyx}) : super(key: keyx); // keyパラメータを追加
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
@override
Widget build(BuildContext context) {
return AppBar(
title: const Text(
'CHATER*ISE',
// 'CHATERAISE(嘘)',
style: TextStyle(color: Colors.brown), // 文字色を白に設定
),
centerTitle: true, // タイトルを中央に配置
backgroundColor: Colors.white, // 背景色を白に設定
);
}
}
ヘッダは状態を持たないのでStatelessWidget
を継承します。
中央に文字を表示したいのでcenterTitle
を用いました。
backgroundColorで背景色を指定しています。
背景色に関してはthemeとして設定できたのかもしれませんが、時間との勝負もあったので、今回はできる範囲で作りました。
フッタの作成
import 'package:flutter/material.dart';
class Footer extends StatefulWidget {
final Function(int) onItemTapped;
const Footer({Key? keyx, required this.onItemTapped}) : super(key: keyx);
@override
FooterState createState() => FooterState();
}
class FooterState extends State<Footer> {
int _selectedIndex = 0; // 現在選択されているアイテムのインデックス
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
unselectedItemColor: Colors.white, // 未選択のアイテムの色を白に設定
selectedItemColor: Colors.cyan,
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.brown, // 背景色を茶色に設定
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_sharp),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.search_sharp),
label: 'Home2',
),
BottomNavigationBarItem(
icon: Icon(Icons.account_box_sharp),
label: 'member',
),
BottomNavigationBarItem(
icon: Icon(Icons.notifications_on_sharp),
label: 'notice',
),
BottomNavigationBarItem(
icon: Icon(Icons.apps_sharp),
label: 'menu',
),
],
currentIndex: _selectedIndex, // 現在選択されているアイテムを指定
onTap: (index) { // タップされたときの処理
setState(() {
_selectedIndex = index;
});
widget.onItemTapped(index); // 親ウィジェットに選択されたインデックスを通知
},
);
}
}
フッタはアイコンの選択状態を保持するためStatefulWidget
を継承しています(あってる?)。
画面下部に配置されるこれは、5つのボタンを横並びで持つため、BottomNavigationBar
を使って並べました。
ボタンひとつひとつはBottomNavigationBarItem
で定めます。icon
とlabel
でアイコン画像と文字を指定しています。
Icons
で指定できるアイコンにはかなりの数があります。vscodeのプラグインのおかげなのか、アイコンのサムネイルが表示されて便利でした。
onTap: (index) { // タップされたときの処理
setState(() {
_selectedIndex = index;
});
widget.onItemTapped(index); // 親ウィジェットに選択されたインデックスを通知
},
ボタンは順にindexを持つため、onTapによってタップ時にindexを受け取り、setStateでindexを保持するようにしています。
なお、 widget.onItemTapped(index);
のonItemTapped
はコールバック関数になります。
メイン処理
flutterでは起動時にmain関数が呼び出されるため、そのときに動作させたい処理を書いていきます。
import 'package:flutter/material.dart';
import 'package:flutter_application/header.dart';
import 'package:flutter_application/footer.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? keyx}) : super(key: keyx);
@override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
int _selectedIndex = 0; // 現在選択されているページのインデックス
void _onItemTapped(int index) { // タップされたときに呼び出される関数
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
final pageOptions = [ // ページのウィジェットをリストで管理
const Placeholder(),
const Placeholder(),
const Placeholder(),
const Placeholder(),
const Placeholder(),
];
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Chateraise Sample',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
cardColor: Colors.white,
scaffoldBackgroundColor: Colors.brown[200]
),
home: Scaffold(
appBar: const Header(),
body: SingleChildScrollView( // SingleChildScrollViewを追加
child: pageOptions[_selectedIndex],
),
bottomNavigationBar: Footer(onItemTapped: _onItemTapped), // コールバック関数を渡す
),
);
}
}
それぞれ見ていきましょう。
main.dart
では前述したheader.dart
とfooter.dart
を使いたいので、importします。
import 'package:flutter_application/header.dart';
import 'package:flutter_application/footer.dart';
相対パスで指定することもできますが、今回は絶対パスで指定しています。
なお、libの記載は不要です。なぜかは調べていません
void _onItemTapped(int index) { // タップされたときに呼び出される関数
setState(() {
_selectedIndex = index;
});
}
MyAppState
クラスに書いたこの処理は、前述のFooterクラスに渡すためのものです。
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Chateraise Sample',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
cardColor: Colors.white,
scaffoldBackgroundColor: Colors.brown[200]
),
home: Scaffold(
appBar: const Header(),
body: SingleChildScrollView( // SingleChildScrollViewを追加
child: pageOptions[_selectedIndex],
),
bottomNavigationBar: Footer(onItemTapped: _onItemTapped), // コールバック関数を渡す
),
);
戻り値MaterialApp
にアプリの構成要素を設定しています。
theme
に指定した値はアプリ全体に作用します
(各々で指定した場合はそちらで上書きされます)。
home
では画面上部にあたるappBar
、メインのコンテンツ部分となるbody
、画面下部にあたるbottomNavigationBar
を設定します。
ここでHeader()やFooter()を設定しています。
...
final pageOptions = [ // ページのウィジェットをリストで管理
const Placeholder(),
const Placeholder(),
const Placeholder(),
const Placeholder(),
const Placeholder(),
];
...
SingleChildScrollView( // SingleChildScrollViewを追加
child: pageOptions[_selectedIndex],
),
_selectedIndex
の値はFooterのボタンを押下したときのindexが設定されてます。
pageOptions
リストにはbodyに設定したいコンテンツのwidgetを定義していますが、こちらはまだ作成していないので、Placeholder()
を指定しておきます。
Placeholder()はまだ実装していないWidgetがあるときに、その領域を×で表示するためのもので、codelabでも使われていました。
画面確認
エラーがないことを確認してから、Ctrl + Shift + P
を押してからのFlutter: Select Device
で開発環境構築時に用意したAndroid端末のエミュレータを選択します。
次にプログラムを実行します。vscodeのメニューバーから実行
->デバッグの開始
で動かしてみます。
おおむね想定通りの内容になりましたね。
Flutterにはホットリロード機能がありますので、エミュレータはこの先立ち上げたままでも大丈夫です。
ホームコンテンツの作成およびリファクタリング(~7.0時間経過)
ここまでは準備で、ここからが本番です。
約4時間、トライアンドエラーを繰り返した結果、見た目だけはそれっぽいものができました。
ディレクトリ構成
.
├── assets
│ └── 適当な画像ファイル ★追加
├── lib
│ └── main.dart ★変更
│ └── footer.dart
│ └── header.dart
│ └── home_contents.dart ★新規作成
├── pubspec.yaml ★変更
And More...
- ホームコンテンツを作るためのファイルを追加しています
- ホームコンテンツを呼び出すためにmain.dartに手を加えています
- 画像を読み込んだり外部パッケージを導入したのでpubspec.yamlも変更が加わります
- 画像ファイルはassets以下に格納
ホームコンテンツの作成
素人なりに四苦八苦しながら作成したソースのため、かなりごちゃごちゃしています。
そこそこ長いので折りたたんでおきました。
home_contents.dartの中身(クリックでオープン)
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:carousel_slider/carousel_slider.dart';
class HomeContents extends StatefulWidget {
const HomeContents({Key? keyx}) : super(key: keyx); // keyパラメータを追加
@override
HomeContentsState createState() => HomeContentsState();
}
class HomeContentsState extends State<HomeContents> {
@override
Widget build(BuildContext context) {
return Column(
children: [
const SizedBox(height: 20.0),
mainContents(),
const SizedBox(height: 10.0),
subContents(),
const SizedBox(height: 20.0),
],
);
}
Widget mainContents() {
return Padding(
padding: const EdgeInsets.only(top: 20.0, left: 20.0, right: 20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start, // 上中央揃えに設定
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Card(
child: ListTile(
title: Center(child: Text('Cashipo会員証')),
subtitle: Center(child: Text('DUMMY')),
),
),
const Text("アプリ会員証の表示方法はこちら(テキスト)"),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // Cardを均等に配置
children: <Widget>[
Expanded(
child: card1(Icons.phone_android_sharp, 'Web予約・店舗受け取り', '商品・受け取り店舗の指定'),
),
Expanded(
child: card1(Icons.store, 'マイページ', '会員向けメニュー'),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
child: card2(Icons.cake_sharp, '商品検索'),
),
Expanded(
child: card2(Icons.new_releases_sharp, '新商品'),
),
Expanded(
child: card2(Icons.calendar_month_sharp, '今月のイベント'),
),
],
),
const Card(
child: ListTile(
title: Row(
mainAxisAlignment: MainAxisAlignment.center, // 中央寄せに設定
children: <Widget>[
Icon(Icons.store),
SizedBox(width: 8.0),
Text('オンラインストア'),
],
),
),
),
],
),
);
}
Widget subContents() {
return Card(
child: Column(
children: <Widget>[
const ListTile(
title: Center(
child: Text(
'RECOMMENDATITON',
style: TextStyle(fontSize: 18.0),
),
),
subtitle: Center(
child: Text(
'おすすめ商品',
style: TextStyle(fontSize: 10.0),
),
),
),
Padding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0),
child: recommendationCard('https://www.google.co.jp/', 'ELL19613004_TP_V4.jpg'),
),
Padding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0),
child: recommendationCard('https://www.google.co.jp/', 'elly063A003_TP_V4.jpg'),
),
CarouselSlider(
items: recommendationCarousel1(),
options: CarouselOptions(
height: 200,
initialPage: 0, //最初に表示するページ番号
autoPlay: true, //自動スライド可否
viewportFraction: 0.6,
enableInfiniteScroll: true, //ループ可否
autoPlayInterval: const Duration(seconds: 4),// 自動表示用設定
autoPlayAnimationDuration: const Duration(milliseconds: 1000), // 自動表示用設定
),
),
const ListTile(
title: Center(
child: Text(
'PROMOTION',
style: TextStyle(fontSize: 18.0),
),
),
subtitle: Center(
child: Text(
'お得情報',
style: TextStyle(fontSize: 10.0),
),
),
),
CarouselSlider(
items: carousel(1),
options: CarouselOptions(
height: 150,
initialPage: 0, //最初に表示するページ番号
autoPlay: false, //自動スライド可否
viewportFraction: 0.5, //表示領域の割合
enableInfiniteScroll: true, //ループ可否
),
),
const ListTile(
title: Center(
child: Text(
'ONLINE SHOP',
style: TextStyle(fontSize: 18.0),
),
),
subtitle: Center(
child: Text(
'オンラインショップ',
style: TextStyle(fontSize: 10.0),
),
),
),
CarouselSlider(
items: carousel(2),
options: CarouselOptions(
height: 150,
initialPage: 0, //最初に表示するページ番号
autoPlay: false, //自動スライド可否
viewportFraction: 0.5, //表示領域の割合
enableInfiniteScroll: true, //ループ可否
),
),
const ListTile(
title: Center(
child: Text(
'ITEM SELECT',
style: TextStyle(fontSize: 18.0),
),
),
subtitle: Center(
child: Text(
'商品選びの参考に',
style: TextStyle(fontSize: 10.0),
),
),
),
Padding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
child: card3('https://www.google.co.jp/', 'aig-ai230706183-xl_TP_V4.jpg')
),
Expanded(
child: card3('https://www.google.co.jp/', 'yudai_509s0044_TP_V4.jpg')
),
],
),
),
const SizedBox(height: 20.0),
],
),
);
}
// アイコンが左にあり、右にタイトルとサブタイトルがあるカード
Widget card1(dynamic icon, dynamic title, dynamic subtitle) {
return Card(
child: ListTile(
leading: Icon(icon),
title: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
title,
style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
),
subtitle: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
subtitle,
style: const TextStyle(fontSize: 12.0),
),
),
),
);
}
// アイコンが上にあり、下にタイトルがあるカード
Widget card2(dynamic icon, dynamic title) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
Icon(icon), // アイコンを追加
Text(
title,
style: const TextStyle(fontSize: 14.0),
),
],
),
),
);
}
// おすすめ商品の画像とリンク
// たとえばAPIで受け取るようにしても良いのかもしれない
Widget recommendationCard(dynamic link, dynamic imgFile) {
return Card(
elevation: 0,
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: InkWell(
onTap: () {
// url_launcherを使用
// 画像をタップしたときのリンク先
launchUrl(Uri.parse(link));
},
child: Stack(
children: [
Container(
width: double.infinity,
height: 260,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: AssetImage('assets/$imgFile'),
fit: BoxFit.cover,
),
),
),
],
),
),
);
}
// その他のおすすめのカルーセル表示用
// マップの情報はAPIで受け取るようにすれば、動的に増やすこともできる
List<Widget> recommendationCarousel1() {
Map<String, String> otherRecommendation = {
'https://www.google.co.jp/' : 'FK+_50A0643_TP_V4.jpg',
'https://www.yahoo.co.jp/' : 'iojima-PB074244162_TP_V4.jpg',
};
return otherRecommendation.entries.map((entry) {
return Card(
elevation: 0,
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: InkWell(
onTap: () {
launchUrl(Uri.parse(entry.key));
},
child: Stack(
children: [
// 商品画像
Container(
width: double.infinity,
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: AssetImage('assets/${entry.value}'),
fit: BoxFit.cover,
),
),
),
],
),
),
);
}).toList();
}
List<Widget> carousel(int mode) {
Map<String, String> datas = {};
switch(mode) {
case 1:
datas = {
'https://www.google.co.jp/' : 'KAZ882_ajisai_TP_V4.jpg',
'https://www.yahoo.co.jp/' : 'PK-PAUI8584_TP_V4.jpg',
};
case 2:
datas = {
'https://www.google.co.jp/' : 'red_sugarA1s8017_TP_V4.jpg',
'https://www.yahoo.co.jp/' : 'yuta12-059_TP_V4.jpg',
};
default:
throw Exception('Failed');
}
return datas.entries.map((entry) {
return Card(
elevation: 0,
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: InkWell(
onTap: () {
launchUrl(Uri.parse(entry.key));
},
child: Stack(
children: [
// 商品画像
Container(
width: double.infinity,
height: 150,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: AssetImage('assets/${entry.value}'),
fit: BoxFit.cover,
),
),
),
],
),
),
);
}).toList();
}
// Cardが画像となり、タップするとリンク先へ飛ぶ
Widget card3(String link, String imgFile) {
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: InkWell(
onTap: () {
launchUrl(Uri.parse(link));
},
child: Stack(
children: [
// 商品画像
Container(
width: double.infinity,
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: AssetImage('assets/$imgFile'),
fit: BoxFit.cover,
),
),
),
],
),
),
);
}
}
長すぎてどこから解説したものか難しいところですが、まずはホームコンテンツの大枠から。
return Column(
children: [
const SizedBox(height: 20.0),
mainContents(),
const SizedBox(height: 10.0),
subContents(),
const SizedBox(height: 20.0),
],
);
SizedBox
で上下に隙間を作っています。
Paddingでも良かったのかもしれませんが、わかりやすかったのでこうしています。
コンテンツはMainとSubに分けており、その間にもSizedBox
で隙間を作っていますね。
以降ではメインとサブのコンテンツのポイントを説明していきます。
mainContentsの処理
child: Column(
mainAxisAlignment: MainAxisAlignment.start, // 上中央揃えに設定
mainAxisSize: MainAxisSize.min,
...
)
Column
は要素を縦に並べるときに使います。mainAxisAlignment
を用いることで表示位置を変えることができます。
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // Cardを均等に配置
children: <Widget>[
...
],
),
Row
は要素を横で並べるときに使います。
こちらもmainAxisAlignment
を用いることで、位置関係を調整しています。
また、Row
で並べるCard
は同じフォーマットにしたかったので、card1(アイコン, タイトル, サブタイル)
関数を作りました。
引数を変えてこれを呼ぶようにすることで、同じ構成のCard
を作ることができます。
(同様の理由でCard2関数も作っています)
なお、参考にした本物では、これらCardをタップすることで別のコンテンツを表示することができますが、今回はそこまで実装を進めることができませんでした。
subContents処理
サブコンテンツ部分ではCardの中でCardを表示したり、カルーセルスライダーを追加したりしました。
順にみていきましょう。
画像Cardを作る
Card(
elevation: 0,
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: InkWell(
onTap: () {
// url_launcherを使用
// 画像をタップしたときのリンク先
launchUrl(Uri.parse(link));
},
child: Stack(
children: [
Container(
width: double.infinity,
height: 260,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: AssetImage('assets/$imgFile'),
fit: BoxFit.cover,
),
),
),
],
),
),
);
こちらはrecommendationCard
関数の処理内容です。
ここではCard
のサイズに合わせた画像を表示し、タップすることで設定したリンク先に遷移することができるようにしています。
Footerのボタンの時にも出てきたOnTapの処理として、launchUrlを呼び出しています。
これはurl_launcherという外部パッケージのもので、引数に指定した遷移先に遷移できるようにするものです。
これを導入するために、コマンドラインから次のコマンドを打ち、url_launcher
を導入しましょう。
$ flutter pub add url_launcher
うまくいけばpubspec.yaml
にurl_launcherが追加されているはずです。
dependencies:
flutter:
sdk: flutter
...
url_launcher: ^6.3.0
導入するだけでは使えないので、使いたいファイルでこれをimport
します。今回はhome_contents.dart
で使っているので、このファイルの先頭に以下を追記します。
import 'package:url_launcher/url_launcher.dart';
これで使えるようになるはずです。
child: Stack(
children: [
Container(
width: double.infinity,
height: 260,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: AssetImage('assets/$imgFile'),
fit: BoxFit.cover,
),
),
),
],
),
ここではCard
のサイズに画像のサイズを合わせています。
(Stackは画像の上に文字を載せようとした名残です。無くても問題ないと思います)
画像の呼び出しはAssetImage('画像パス')
になります。
ただし、画像の格納先は事前にpubspec.yaml
で指定しておく必要があります。
flutter:
...
# To add assets to your application, add an assets section, like this:
assets:
- assets/
カルーセルスライダーを導入する
参考にした本物のアプリ側では、画像Cardの下に、自動で横にスライドする画像Cardが並んでいました。
これを実現するため、carousel_slider
を使います。
url_launcher
の時と同じように、carousel_slider
をプロジェクトに追加します。
$ flutter pub add carousel_slider
pubspec.yaml
に追記されていることも見てみましょう。
dependencies:
flutter:
...
carousel_slider: ^4.2.1
import
するのも忘れずに。
import 'package:carousel_slider/carousel_slider.dart';
実際にカルーセルを設定している処理はこの部分。
CarouselSlider(
items: recommendationCarousel1(),
options: CarouselOptions(
height: 200,
initialPage: 0, //最初に表示するページ番号
autoPlay: true, //自動スライド可否
viewportFraction: 0.6,
enableInfiniteScroll: true, //ループ可否
autoPlayInterval: const Duration(seconds: 4),// 自動表示用設定
autoPlayAnimationDuration: const Duration(milliseconds: 1000), // 自動表示用設定
),
),
items
にはCardのリストを設定しますが、Cardのフォーマットは画像とリンク先以外は同じものであるため、recommendationCarousel1()
として関数化しました。
option
ではカルーセルの動作条件などを指定できます。
ここでは自動スライドさせたかったので、autoPlay
をtrueにしています。また、その動作間隔なども設定しています。
recommendationCarousel1()
関数で作成しているCardリストの内容は「画像Card」のものと同じですが、ここではカルーセル動作のためにCardがいくつも必要になるため、Mapを用いて複数のCardを作って返しています。
Map<String, String> otherRecommendation = {
'https://www.google.co.jp/' : 'FK+_50A0643_TP_V4.jpg',
'https://www.yahoo.co.jp/' : 'iojima-PB074244162_TP_V4.jpg',
};
return otherRecommendation.entries.map((entry) {
return Card(
...
launchUrl(Uri.parse(entry.key));
...
image: AssetImage('assets/${entry.value}'),
...
);
}).toList();
map()を用いることでIterableな処理が可能になります。
以上がホームコンテンツのだいたいの実装内容になります。
メイン処理の修正
最後に、ホームコンテンツを呼び出すためにmain.dartを直します。
...
import 'package:flutter_application/home_contents.dart'; // 追加
...
@override
Widget build(BuildContext context) {
final pageOptions = [
const HomeContents(), // 変更
const Placeholder(),
const Placeholder(),
const Placeholder(),
const Placeholder(),
];
...}
動作確認
assets
ディレクトリ以下にhome_contents.dart
で指定している画像ファイルを配置してから、実際に動かしてみましょう。
次のような画面が出ているはずです。
画像では伝わりませんが、画像Cardをタップすることで画面遷移を確認することができます(仮なのでgoogleかyahooのトップに遷移します)。
また、carousel_sliderが正常に機能(自動で切り替わること、スワイプして切り替えられること)していることも確認できました。
まとめ
思い立ったが吉日でflutterでのモバイルアプリ開発を行ってみましたが、やはり難しかったですね。
思ったようにwidgetが配置できなかったり、表示が崩れたり…(おそらく今も端末によっては崩れたり不格好になると思います)。
しかしながら、これを使いこなすことができればクロスプラットフォームに対応したアプリ開発ができるようになるはずなので、妄想は膨らみますね。
とはいえやっぱり、こういうアプリをこの先作ろうとするなら、yappliとか使うのが一番なんでしょうね…。
あるもの使っていけばいい(全部ぶち壊し)。