Flutter を勉強する
Flutter を一から勉強する。
Flutter のインストール
- Flutter をインストール(公式の手順をみるのが良さそう)
-
flutter doctor
で設定状況が確認できる
↑ Android と iOS(Xcode) だけダメっぽかったので設定をする
iOS (Xcode)
-
cocoapods
が最新版じゃないというエラーが出てたCocoaPods 1.9.1 out of date (1.11.0 is recommended).
To upgrade see https://guides.cocoapods.org/using/getting-started.html#installation for instructions.
- とりあえず xcode を最新版にする
- インストール手順に沿って入れてみた
- https://guides.cocoapods.org/using/getting-started.html#installation
-
ERROR: Error installing cocoapods: ERROR: Failed to build gem native extension.
というエラーで止まる
- ruby を最新にする
- 参考
brew update
brew install rbenv ruby-build
brew install libyaml
rbenv install 3.2.0
rbenv global 3.2.0
source ~/.zshrc
-
ruby -v
→3.2.0
- できた
- もう一度
cocoapods
を入れてみたらエラーがなくなって、Xcode の準備もできた!
Android
エラーはこれ
Android toolchain - develop for Android devices (Android SDK version 29.0.3)
✗ cmdline-tools component is missing
Run `path/to/sdkmanager --install "cmdline-tools;latest"`
See https://developer.android.com/studio/command-line for more details.
✗ Android license status unknown.
Run `flutter doctor --android-licenses` to accept the SDK licenses.
See https://flutter.dev/docs/get-started/install/macos#android-setup for more details.
- 過去に Android Studio は入れたことあったので起動してみた
- アップデートがありそうだったので最新版をダウンロード
- 公式サイトで新しいのを落とす感じっぽいけど、長い...
- 環境変数を設定した
- アップデートがありそうだったので最新版をダウンロード
- command-line tools は Android Studio を開いて
More Action
→SDK Manager
→SDK Tools
→Android SDK Command-line Tools(latest)
を選択で一つは解決 - もう一つのエラーは
flutter doctor --android-licenses
を叩いて全部許可すればOK -
Unable to find bundled Java version.
は以下の記事を参考にしたらできた- https://zenn.dev/sd7jp/articles/763c84e398c6c7
cd /Applications/Android\ Studio.app/Contents
-
ln -nfs jre jdk
これで可決!
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.7.1, on macOS 12.6.3 21G419 darwin-x64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
[✓] Xcode - develop for iOS and macOS (Xcode 14.2)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.1)
[✓] VS Code (version 1.74.2)
[✓] Connected device (2 available)
[✓] HTTP Host Availability
• No issues found!
アプリ起動
以下で作成
flutter create [myapp]
Null案件を導入
以下のコマンドで ?
での null 表現ができるようになる
dart migrate --apply-changes
アプリをコマンドから実行
VSCodeのステータスバーに対象のデバイスがでるのでそれで選ぶ
flutter devices # 起動しているデバイスを確認
flutter run --device-id chrome # chromeを起動 (F5キーでも可)
↓ ローカルホストが立ち上がった
http://localhost:63215/#/
q
キーで停止
Widgetとは
Flutter の UI を構築しているパーツを Widget
と呼ぶ
Widget はツリー状になっている(HTMLみたいな感じかな)
テキストを表示
Text('Default')
スタイル
色々なスタイルの指定方法
Text('Bold', style: TextStyle(fontWeight: FontWeight.bold))
Text('Italic', style: TextStyle(fontStyle: FontStyle.italic))
Text('fontSize = 36', style: TextStyle(fontSize: 36))
Text('Red', style: TextStyle(color: Colors.red))
位置
Text('TextAlign.right', textAlign: TextAlign.right)
Container
要素を囲って余白とかサイズとかを決める要素
線を引くには decoration
を使う
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.blue, width: 2),
borderRadius: BorderRadius.circular(8),
),
child: Text('border')
)
余白、色とかはcssみたいな感じ
padding とかで方向ごとに決めるときは EdgeInsets.only()
を使えそう
Container(
color: Colors.blue,
width: 200,
height: 50,
padding: EdgeInsets.only(left: 20, top: 8, bottom: 8, right: 20),
margin: EdgeInsets.all(8),
child: Text('container')
)
背景画像はこんな感じ
ローカルの画像を指定するときは AssetImage('image_path')
とのこと
画像をかくだいするには BoxFit
を使う(contain
, cover
, fitWidth
, fitHeight
, scaleDown
, none
)
Container(
width: 400,
height: 400,
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage('https://source.unsplash.com/960x540/?city,sky')
fit: BoxFit.fill,
)
),
)
縦、横並び
縦並びは Column
、横並びは Row
を使う
Column(
mainAxisAlignment: MainAxisAlignment.center, //
children: <Widget>[
Text('fisrt'),
Text('seconds'),
]
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('fisrt'),
Text('seconds'),
]
),
ボタン
-
TextButton
テキストボタン-
onPressed
にクリック時の処理を書く - 実行したくない場合は
null
をセットする
-
-
OutlinedButton
枠線ありボタン -
ElevatedButton
影ありボタン -
IconButton
アイコンボタン-
TextButton.icon
他のボタンでアイコンをつけるとき
-
-
Scaffold
+FloatingActionButton
フローティングのボタンを作るとき-
Scaffold
はUIのベースとなるWidget - 白紙のキャンバスみたいなイメージ
-
TextButton(
onPressed: () {},
style: TextButton.styleFrom(primary: Colors.red),
child: const Text('影なしボタン')
),
OutlinedButton(
onPressed: () {},
style: OutlinedButton.styleFrom(primary: Colors.red),
child: const Text('枠線ボタン')
),
ElevatedButton(
onPressed: null,
style: ElevatedButton.styleFrom(
primary: Colors.red, elevation: 16),
child: const Text('影ありボタン')
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.thumb_up),
color: Colors.pink,
iconSize: 64
),
ElevatedButton.icon(
onPressed: null,
style: ElevatedButton.styleFrom(
primary: Colors.red, elevation: 16),
icon: const Icon(Icons.thumb_up_sharp),
label: const Text('影ありボタン + icon')
),
リスト
-
ListView
の children に要素を並べるとスクロール可能なリストが作れる -
ListView.builder
を使うと中身を変数で定義してそれをもとにリストを作ることができる -
ListTile
,Card
を使って見た目をいい感じに作ることもできる
final listItems = [
'apple',
'orange',
'banana',
];
ListView.builder(
itemCount: listItems.length,
itemBuilder: (context, index) {
return Text(listItems[index]));
}),
)
Card(
child: ListTile(
title: Text('list title'), subtitle: Text('sub title')
),
),
AppBar
よくあるヘッダーのバー
AppBar
を使う
Scaffold(
appBar: AppBar(
// 左側のアイコン
leading: Icon(Icons.arrow_back),
// タイトルテキスト
title: Text('Hello'),
// 右側のアイコン一覧
actions: <Widget>[
IconButton(
onPressed: () {},
icon: Icon(Icons.favorite),
),
IconButton(
onPressed: () {},
icon: Icon(Icons.more_vert),
),
],
),
)
NetworkImage
を使って外部リソースを読み込もうとしたらcorsエラーが出た
Flutter にはレンダー方式が2つあって、自動では canvaskitレンダー
という方式らしい
canavs で外部リソースを読み込むと確かにエラーになるのでそれのせいっぽい
HTMLレンダー
と言う方を使えば良いみたい
↓起動時に以下のコマンドを叩くと表示された
flutter run -d chrome --web-renderer html
本番用のビルドも変わる
flutter build web --web-renderer html
パッケージのインストールのやり方
- pub.dev でパッケージを探す
-
flutter pub add [パッケージ名]
でパッケージをインストール。もしくはpubspec.yaml
のdependencies
にパッケージ情報を追記してからflutter pub get
コマンドでパッケージをインストールする - 使いたいところで
import
して使う
参考
npm scripts みたいによく使うコマンドを定義したい
これでできるみたい
グローバルにインストール
dart pub global activate rps
.zshrc
に以下を追加
パスが通ってないので export PATH="$PATH":"$HOME/.pub-cache/bin"
pubspec.yaml
に scripts
を追加
scripts:
run: "flutter run -d chrome --web-renderer html"
実行
rps run
動いた!
状態を持った Widget
-
StatefulWidget
を継承した Widget -
State
を継承したデータ
// StatefulWidgetを継承するとStateを扱える
// このWidgetを表示すると、Stateを元にUIが作成される
class MyWidget extends StatefulWidget {
// 使用するStateを指定
_MyWidgetState createState() => _MyWidgetState();
}
// Stateを継承して使う
class _MyWidgetState extends State<MyWidget> {
// データを宣言
int count = 0;
// データを元にWidgetを作る
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text(count.toString()),
RaisedButton(
onPressed: () {
// データを更新する時は setState を呼ぶ
setState(() {
// データを更新
count = count + 1;
});
},
child: Text('カウントアップ'),
),
],
);
}
}
TODOを作る
ページを作る
一旦、特に何でもないページを作成
import 'package:flutter/material.dart';
void main() {
// 最初に表示するWidged
runApp(MyTodoApp());
}
// アプリ自体
class MyTodoApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'My Todo App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: TodoListPage(),
);
}
}
// トップページに表示するページ
class TodoListPage extends StatelessWidget {
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('リスト一画面')
),
);
}
}
ページ遷移を作る
- もう一つページを作って
Navigator.on(context).push()
とMaterialPageRoute()
を使って遷移させる - ページを戻るだけなら
Navigator.of(context).pop()
で良いみたい
/// リスト一覧ページ
class TodoListPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: const Center(
child: Text('リスト一画面')
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// "push" で新規画面に遷移
Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
return TodoAddPage();
}),
);
},
child: const Icon(Icons.add),
),
);
}
}
/// 追加ページ
class TodoAddPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('リスト追加画面(クリックで戻る)')
)
);
}
}
リスト表示を作る
-
ListView
,Card
,ListTile
を使って作る
/// リスト一覧ページ
class TodoListPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('リスト一覧')
),
body: ListView(
children: const <Widget>[
Card(
child: ListTile(
title: Text('にんじんを買う'),
)
),
Card(
child: ListTile(
title: Text('にんじんを買う'),
)
),
Card(
child: ListTile(
title: Text('にんじんを買う'),
)
),
Card(
child: ListTile(
title: Text('にんじんを買う'),
)
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// "push" で新規画面に遷移
Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
return TodoAddPage();
}),
);
},
child: const Icon(Icons.add),
),
);
}
}
タスクを入力するフォームを作る
-
TodoAddPage
の継承をStatefulWidget
に変更する -
_TodoAddPageState
クラスを作ってState<TodoAddPage>
を継承する-
String _text = '';
として状態を作る -
TextField
のonChanged
で値の変更を受け取り、setState()
で変数にセット
-
-
Navigator.of(context).pop(_text);
でページを戻る時に値を返す
/// 追加ページ
class TodoAddPage extends StatefulWidget {
_TodoAddPageState createState() => _TodoAddPageState();
}
class _TodoAddPageState extends State<TodoAddPage> {
String _text = '';
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('リスト追加')
),
body: Container(
padding: const EdgeInsets.all(64),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(_text, style: const TextStyle(color: Colors.blue)),
TextField(
onChanged: (String value) {
setState(() {
_text = value;
});
}
),
const SizedBox(height: 8),
Container(
width: double.infinity, // 横いっぱいに広げる
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop(_text); // 変更後のテキストを返す
},
child: const Text('リスト追加', style: TextStyle(color: Colors.white))
)
),
const SizedBox(height: 8),
Container(
width: double.infinity,
child: TextButton(
onPressed: () {},
child: const Text('キャンセル'),
)
)
]
)
)
);
}
}
一覧ページでデータを受け取る
-
onPressed
をasync
にしてNavigator.of().push()
をawait
にする - その引数を変数
newListText
に入れることで、TodoAddPage から返ってきた時の値を受け取れる
class TodoListPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('リスト一覧')
),
body: ListView(
children: const <Widget>[
Card(
child: ListTile(
title: Text('にんじんを買う'),
)
),
Card(
child: ListTile(
title: Text('にんじんを買う'),
)
),
Card(
child: ListTile(
title: Text('にんじんを買う'),
)
),
Card(
child: ListTile(
title: Text('にんじんを買う'),
)
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
// 返ってきた時の値を受け取る
final newListText = await Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
return TodoAddPage();
}),
);
if (newListText != null) {
//
}
},
child: const Icon(Icons.add),
),
);
}
}
受け取ったデータを表示する
-
List<string> todoList = []
と変数を定義する - 返ってきた値を受け取ったら追加するようにする
setState(() { todoList.add(newListText) });
-
ListView.builder()
でtodoList
の中身を表示する
/// リスト一覧ページ
class TodoListPage extends StatefulWidget {
_TodoListPageState createState() => _TodoListPageState();
}
class _TodoListPageState extends State<TodoListPage> {
List<String> todoList = [];
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('リスト一覧')
),
body: ListView.builder(
itemCount: todoList.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
title: Text(todoList[index]),
)
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
// かえってきた時の値を受け取る
final newListText = await Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
return TodoAddPage();
}),
);
if (newListText != null) {
setState(() {
todoList.add(newListText);
});
}
},
child: const Icon(Icons.add),
),
);
}
}
vim で開発する
VSCode だとなんか重いなーってたけど以下の方法で環境を作って Vim で開発したら快適だった
- coc を使っているので
:CocInstall coc-flutter
でインストールする - シンタックスハイライトのために以下をインストール(vim-plug)
Plug 'dart-lang/dart-vim-plugin'
Plug 'thosakwe/vim-flutter'
Firebase を使う
プロジェクトを作るのはいつも通り
セットアップ
firebase 側の公式があるからこれがいいかも...
firebase にログイン
firebase login
flutterfire_cli
をグローバルで有効化する
dart pub global activate flutterfire_cli
flutterfire_cli
の参考はこれ
あとは以下のコマンドでfirebaseのプロジェクトを選択し、プラットフォームを全部選択し、android/build.gradleの更新を行うかどうかを yes にしてセットアップする
これで lib/firebase_options.dart
が作られてそこに firebase のオプションがセットアップされているファイルが作られる...すごい...
flutterfire configure
firebase_core
をインストール
flutter pub add firebase_core
main.dart
に import して、初期化の処理を入れる
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(MyTodoApp());
}
Firebase Authentication
Firebase側で Auth の設定をする
今回使いたいのは Google ログイン
Firebase の構成を変えたら以下のコマンドで最新の状態を取ってくるのが良さそう
flutterfire configure
あとは使うパッケージをインストール
flutter pub add firebase_auth google_sign_in sign_in_button
処理はこんな感じ
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:sign_in_button/sign_in_button.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'firebase_options.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(MyTodoApp());
}
/// アプリのベース
class MyTodoApp extends StatelessWidget {
final String title = 'murmur';
Widget build(BuildContext context) {
return MaterialApp(
title: title,
theme: ThemeData(
primarySwatch: Colors.blue, // テーマーカラーを設定
),
home: TopPage(), // 表示するページを指定
);
}
}
class TopPage extends StatefulWidget {
State<TopPage> createState() => _TopPageState();
}
class _TopPageState extends State<TopPage> {
Future<UserCredential> signInWithGoogle() async {
// Trigger the authentication flow
final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn();
// Obtain the auth details from the request
final GoogleSignInAuthentication? googleAuth = await googleUser?.authentication;
// Create a new credential
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth?.accessToken,
idToken: googleAuth?.idToken,
);
// ログイン後に遷移する
Navigator.push(context, MaterialPageRoute(builder: (context) => TodoListPage()));
// Once signed in, return the UserCredential
return await FirebaseAuth.instance.signInWithCredential(credential);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ログイン')
),
body: Center(
child:
Container(
width: 200,
height: 30,
child: SignInButton(Buttons.google, onPressed: () {
signInWithGoogle();
}),
),
)
);
}
}
/// リスト一覧ページ
class TodoListPage extends StatefulWidget {
_TodoListPageState createState() => _TodoListPageState();
}
class _TodoListPageState extends State<TodoListPage> {
List<String> todoList = [];
final _auth = FirebaseAuth.instance;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('リスト一覧'),
actions: [
IconButton(
onPressed: () async {
await _auth.signOut();
Navigator.pop(context);
},
icon: const Icon(Icons.arrow_back)
)
]
),
body: ListView.builder(
itemCount: todoList.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
title: Text(todoList[index]),
)
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
// かえってきた時の値を受け取る
final newListText = await Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
return TodoAddPage();
}),
);
if (newListText != null) {
setState(() {
todoList.add(newListText);
});
}
},
child: const Icon(Icons.add),
),
);
}
}
web で動かしてる時に以下のエラーが出た
"ClientID not set. Either set it on a <meta name="google-signin-client_id" content="CLIENT_ID" /> tag, or pass clientId when calling init()"
↓この手順でクライアントIDとかを調べて入れたら動いた。アプリはわからん...
試せてないもの
- ログインしてなかったらトップページに戻る
- アプリでのログイン
iOS のシミュレーターで実行する方法
以下のコマンドでシミュレーターを起動する
open -a Simulator
以下のコマンドでデバイスリストを表示する。シミュレーターが起動していればそのデバイスも表示される。
flutter devices
その中から選んで以下のコマンドを実行するとシミュレーター上にアプリがデプロイされる(結構時間かかるっぽい)
flutter run -d BE1D1287-B81A-4BDD-8B62-5486E2018854 #←これはきっと毎回違う乱数
iOSのデバッグはどうしてるんだろう
シミュレーターめっちゃ重いけど...
実機にもどうやって入れるのか
起動時のポート番号を固定する
デフォルトでは毎回変わってしまっているっぽい
Fireabse を使う際に指定している ClientId がURLを指定しないといけないのでポートが変わると使えなくなってしまうので固定する
起動時に --web-port
というオプションで設定できる
以下のような形で落ち着いた
flutter run -d chrome --web-renderer html --web-port 5000
Firestore
必要なライブラリを落としてくる
flutter pub add cloud_firestore
インポートして、こんな感じで使う
使うAPIはJSとかとほぼ変わらずかけそう
import 'package:cloud_firestore/cloud_firestore.dart';
final db = FirebaseFirestore.instance;
final userID = FirebaseAuth.instance.currentUser?.uid;
List todoList = [];
try {
await db.collection('users').doc(userID).get().then((event) {
if (event.exists) {
todoList = data;
}
});
} catch(e) {
print('Error: $e');
}
参考サイトのほぼそのままだけど使いやすい感じにクラス化して使った
クラスを作った
class FirestoreService {
final db = FirebaseFirestore.instance;
final userID = FirebaseAuth.instance.currentUser?.uid ?? '';
Future getUser() async {
try {
return db.collection('users').doc(userID).get().then((event) async {
print(userID);
// ユーザーデータがないときはデータを作成する
if (!event.exists && userID is String) {
await db.collection('users').doc(userID).set({
'todo': [
{
'body': 'ようこそ!',
'timestamp': Timestamp.now()
}
]
});
}
});
} catch(e) {
print('Error: $e');
}
}
void get(WidgetRef ref) async {
try {
await db.collection('users').doc(userID).get().then((event) {
final data = event.get('todo');
if (event.exists) {
ref.read(todoProvider.notifier).state = data;
} else {
ref.read(todoProvider.notifier).state = [];
}
});
} catch(e) {
print('Error: $e');
}
}
void add(WidgetRef ref, String value) async {
final timestamp = Timestamp.now();
final oldList = ref.read(todoProvider);
final Map<String, List<dynamic>> todoMap = {
'todo' : [
...oldList,
{
'body': value,
'timestamp': timestamp,
},
]
};
try {
db.collection('users').doc(userID).set(todoMap);
} catch(e) {
print('Error: $e');
}
}
void delete() async {
try {
db.collection('users').doc(userID).delete().then((doc) => null);
} catch(e) {
print('Error: $e');
}
}
}
Provider
↑の例でも使ってるけど、アプリ全体でデータを共有するために使うやつっぽい(全然わかってない)
こんな感じで todoProvider
を作って runApp()
の時にアプリを囲む
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(ProviderScope(child: MyTodoApp()));
}
final todoProvider = StateNotifierProvider.autoDispose<Todo, List<dynamic>>((ref) {
return Todo();
});
class Todo extends StateNotifier<List<dynamic>> {
Todo() : super([]);
}
使いたいページを CunsumerStatefulWidget
と ConsumerState
で作ると、ref
が使えるようになるので、その ref を使ってprovider の変化とかを検知して使う
class TodoListPage extends ConsumerStatefulWidget {
_TodoListPageState createState() => _TodoListPageState();
}
class _TodoListPageState extends ConsumerState<TodoListPage> {
void initState() {
super.initState();
init();
}
List<String> todoList = [];
void init () async {
FirestoreService().get(ref);
}
Widget build(BuildContext context) {
final list = ref.watch(todoProvider);
return Scaffold(
body:
ListView.builder(
itemCount: list.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
title: Text(list[list.length]['body']),
subtitle: Text(list[list.length]['timestamp'].toString()),
)
);
},
),
);
}
async/await
JS の promise
みたいなもので Future
というのがあるらしいのでこれを使う