- 投稿日:2021-01-01T23:34:13+09:00
Flutter初心者に必要な最低限の知識
もくじ
- はじめに
- 作成したコードの紹介
- 基本構成
- 動的に表示を変更
- 画面遷移
- 抑えておくべき記法
- さいごに
はじめに
Javaの経験はありますが、flutterおよびdartは初心者の私がアプリ作成までに学んだ、最低限これだけは抑えておくべき内容を厳選してまとめておきます。
「初心者だけど、とにかく早くアプリが作成したい」「flutter始めたいけど調べるのが面倒くさい」なんて人にはおすすめできる内容になっていると思っています。
この記事が誰かのお役に立てれば幸いです。作成したコードの紹介
作成してみたflutterアプリのmainファイルを最下部に記載しました。
動的な表示変更、画面遷移、ファイル読み込みを行っています。基本構成
動かすために必要なクラスがあります。
下記の3つのクラスです。メイン関数
void main()ひとつめはmain関数。
プログラムが実行されると始めに実行される部分になります。アプリケーションクラス
class MyApp extends StatelessWidget2つ目はアプリケーションを定義するクラス。
このクラスでhome画面を指定したりします。静的な画面クラス
class MenuPage extends StatelessWidget3つ目は画面を定義するクラス。
上記は静的な画面の場合になります。動的な画面クラス
class ScenarioPageLapper extends StatefulWidget class _ScenarioPage extends State<ScenarioPageLapper>動的な画面を使用する場合は上記2つのクラスが必要です。
動的に表示を変更
動的に表示内容を変更する場合は下記のように2箇所記述します。
setState(() { this.text = "changed"; });1箇所目はsetStateメソッドです。
このメソッドを呼び出すと表示内容が変更されます。child: Text('$text'),2箇所目は表示を変える部分です。
上記のように記載することで、動的に表示が変わります。画面遷移
画面遷移させるには下記のように記載します。
Navigator.push( context, MaterialPageRoute(builder: (context) => ScenarioPageLapper(title: "タイトル")), );上記はサブ画面を呼び出す例。
現在開いているページの上に新しいページを重ねるイメージです。Navigator.pop(context);上記はサブ画面を閉じる例。
主に戻る際の処理として利用します。
前述した通り、Navigator.pushを行うとページが重なっていくため、Navigator.pushのみで画面遷移を行うと何重にもページが重なってしまいます。抑えておくべき記法
Java経験者の私が抑えておくと良いと思うdartの記法をまとめてみました。
セッターの省略
Sample(this.name)Sample(String name) { this.name = name; }上2つのコードは結果は同じです。
上記のように記述することでセッターが省略できます。記述の省略
State<StatefulWidget> createState() => _ScenarioPage(title);State<StatefulWidget> createState() { return _ScenarioPage(title); }上2つのコードは結果は同じです。
上記のように記述することができます。初期化子
ScenarioPageLapper({Key key, this.title}) : super(key: key);初期化子。
上記のように記述することで、コンストラクタの先頭でsuper(key: key)が実行されます。非同期の処理
ファイル読み込み処理などは非同期の処理を利用する方法が一般的なようです。
下記のような感じになります。
Flutterでは非同期処理は頻出のようです。(Androidアプリでもそうだった気もしますが)var result = getFileText(); result.then((content) => this.sc = new Scenario(content)); Future<String> getFileText() { return rootBundle.loadString(SC_FILE_PATH + this.scName + SC_FILE_EXT); }ホットリロード機能
便利なホットリロード機能というものがあります。
デバッグしながらコードを修正してリアルタイムに反映させることができます。
Terminalで"flutter run"のコマンドを実行するのではなく、VSCodeであればF5キーのデバッグ機能で実行します。さいごに
flutterを手っ取り早く開発したい人向けに、最低限のポイントを厳選して記載してみました。
この記事がお役に立てれば幸いです。コード
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'util.dart'; import 'scenario.dart'; import 'const.dart'; /// ================================================== /// メイン処理 /// ================================================== void main() { runApp(MyApp()); } /// ================================================== /// アプリケーション定義 /// ================================================== class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: TITLE, theme: ThemeData( primarySwatch: Colors.blue, ), home: MenuPage(), ); } } /// ================================================== /// メニュー画面 /// ================================================== class MenuPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( children: <Widget>[ // 画像エリア Container( // 最大横幅の90% width: MediaQuery.of(context).size.width * (90 / 100), // 最大高さの60% height: MediaQuery.of(context).size.height * (60 / 100), // ボーダー decoration: BoxDecoration( border: Border.all(color: Colors.black), ), // パディング padding: const EdgeInsets.all(5.0), // マージン margin: EdgeInsets.all(5.0), ), // ボタンエリア1 Container( // 最大横幅の90% width: MediaQuery.of(context).size.width * (90 / 100), // 最大高さの15% height: MediaQuery.of(context).size.height * (15 / 100), // パディング padding: const EdgeInsets.all(5.0), // マージン margin: EdgeInsets.all(5.0), child: Row( // 等間隔に並べる mainAxisAlignment: MainAxisAlignment.spaceAround, // ウィジット郡 children: <Widget>[ // Episode1 RaisedButton( child: const Text(TEXT_SCENARIO_1), color: HexColor(BGCOLOR_SCENARIO), textColor: Colors.black, onPressed: () { // シナリオ画面に遷移 Navigator.push( context, MaterialPageRoute(builder: (context) => ScenarioPageLapper(title: SC_FILE_NAME_1)), ); }, ), // Episode2 RaisedButton( child: const Text(TEXT_SCENARIO_2), color: HexColor(BGCOLOR_SCENARIO), textColor: Colors.black, onPressed: () { // シナリオ画面に遷移 }, ), // Episode3 RaisedButton( child: const Text(TEXT_SCENARIO_3), color: HexColor(BGCOLOR_SCENARIO), textColor: Colors.black, onPressed: () { // シナリオ画面に遷移 }, ), ] ) ), // ボタンエリア2 Container( // 最大横幅の90% width: MediaQuery.of(context).size.width * (90 / 100), // 最大高さの15% height: MediaQuery.of(context).size.height * (15 / 100), // パディング padding: const EdgeInsets.all(5.0), // マージン margin: EdgeInsets.all(5.0), child: Row( // 等間隔に並べる mainAxisAlignment: MainAxisAlignment.spaceAround, // ウィジット郡 children: <Widget>[ // Episode4 RaisedButton( child: const Text(TEXT_SCENARIO_4), color: HexColor(BGCOLOR_SCENARIO), textColor: Colors.black, onPressed: () { // シナリオ画面に遷移 }, ), // Episode5 RaisedButton( child: const Text(TEXT_SCENARIO_5), color: HexColor(BGCOLOR_SCENARIO), textColor: Colors.black, onPressed: () { // シナリオ画面に遷移 }, ), // Episode6 RaisedButton( child: const Text(TEXT_SCENARIO_6), color: HexColor(BGCOLOR_SCENARIO), textColor: Colors.black, onPressed: () { // シナリオ画面に遷移 }, ), ] ) ), ] ) ) ); } } /// ================================================== /// シナリオ画面ラッパー /// ================================================== class ScenarioPageLapper extends StatefulWidget { /// タイトル final String title; /// ========================= /// コンストラクタ /// ========================= ScenarioPageLapper({Key key, this.title}) : super(key: key); @override State<StatefulWidget> createState() => _ScenarioPage(title); } /// ================================================== /// シナリオ画面 /// ================================================== class _ScenarioPage extends State<ScenarioPageLapper> { /// シナリオ名 String scName; /// シナリオ Scenario sc; /// 描画データ ViewData viewData; /// 画像URL String imgUrl; /// テキスト String text; /// ========================= /// コンストラクタ /// ========================= _ScenarioPage(String scName) { // シナリオ名を設定 this.scName = scName; // 非同期でファイルからテキストを取得してシナリオを初期化 var result = getFileText(); result.then((content) => this.sc = new Scenario(content)); // 初期表示画像 this.imgUrl = IMG_ROOT_PATH + INIT_IMG_NAME; // 初期表示テキスト this.text = INIT_TEXT; } /// ========================= /// 描画更新 /// ========================= void updateView() { setState(() { switch (this.viewData.dataType.toString()) { case SC_TYPE_IMG: this.imgUrl = this.viewData.value; break; case SC_TYPE_FADEIN: this.imgUrl = this.viewData.value; break; case SC_TYPE_CUTIN: this.imgUrl = this.viewData.value; break; default: this.text = this.viewData.value; // 音声が指定されていれば音声再生 if (this.viewData.audio != "") { // this.player.play(this.viewData.audio); } } }); } /// ========================= /// ★★★非同期処理★★★ /// ファイルからテキストを取得 /// ========================= Future<String> getFileText() { return rootBundle.loadString(SC_FILE_PATH + this.scName + SC_FILE_EXT); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( children: <Widget>[ // 画像エリア Container( // 最大横幅の90% width: MediaQuery.of(context).size.width * (90 / 100), // 最大高さの60% height: MediaQuery.of(context).size.height * (70 / 100), // ボーダー decoration: BoxDecoration( border: Border.all(color: Colors.black), ), // パディング padding: const EdgeInsets.all(5.0), // マージン margin: EdgeInsets.all(5.0), // 画像 動的に変更される child: Image.asset('$imgUrl'), ), // テキストエリア Container( // 最大横幅の90% width: MediaQuery.of(context).size.width * (90 / 100), // 最大高さの20% height: MediaQuery.of(context).size.height * (15 / 100), // デコレーション decoration: BoxDecoration( // ボーダー border: Border.all(color: Colors.black), // カラー color: HexColor(BGCOLOR_TEXT_EREA), ), // パディング padding: const EdgeInsets.all(5.0), // マージン margin: EdgeInsets.all(5.0), // テキストエリア 動的に変更される child: Text('$text'), ), // ボタンエリア Container( // 最大横幅の90% width: MediaQuery.of(context).size.width * (90 / 100), // 最大高さの10% height: MediaQuery.of(context).size.height * (10 / 100), // パディング padding: const EdgeInsets.all(5.0), // マージン margin: EdgeInsets.all(5.0), child: Row( // 等間隔に並べる mainAxisAlignment: MainAxisAlignment.spaceAround, // ウィジット郡 children: <Widget>[ // 戻るボタン RaisedButton( child: const Text(TEXT_PREV_BTN), color: HexColor(BGCOLOR_PREV_BTN), shape: const StadiumBorder(), onPressed: () { // 前の描画データを取得 this.viewData = this.sc.getPrevViewData(); this.updateView(); }, ), // メニューへ戻るボタン RaisedButton( child: const Text(TEXT_MENU_BTN), color: HexColor(BGCOLOR_MENU_BTN), textColor: Colors.black, onPressed: () { // メニュー画面を表示 Navigator.pop(context); }, ), // 進むボタン RaisedButton( child: const Text(TEXT_NEXT_BTN), color: HexColor(BGCOLOR_NEXT_BTN), shape: const StadiumBorder(), onPressed: () { // 次の描画データを取得 print(this.sc); this.viewData = this.sc.getNextViewData(); this.updateView(); }, ), ] ) ), ] ) ) ); } }
- 投稿日:2021-01-01T15:16:42+09:00
【Android / Kotlin】フォーム(EditText)入力の際、背景タップで表示キーボードを閉じる
はじめに
Androidアプリ開発(Kotlin)で学んだ内容を備忘録として残します!
どのような機能か?
場面
- フォーム(EditText)にフォーカスが当たっている
- 入力用キーボードが表示されている
動作
上記の状態で背景画面をタップするとキーボードを閉じる
というもの
実装
Activity
MainActivity.kt// ~ 省略 ~ // Viewがタッチされる時に処理が走るメソッド override fun onTouchEvent(event: MotionEvent?): Boolean { // InputMethodManager をキャストしながら取得 val inputMethodManager: InputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager // エルビス演算子でViewを取得できなければ return false // focusViewには入力しようとしているのEditTextが取得されるはず val focusView = currentFocus ?: return false // このメソッドでキーボードを閉じる inputMethodManager.hideSoftInputFromWindow( focusView.windowToken, InputMethodManager.HIDE_NOT_ALWAYS ) return false }これだけ。
ちなみに
キーボードを閉じるメソッドは
InputMethodManagerクラス
ではこのように定義されている。InputMethodManager.javapublic boolean hideSoftInputFromWindow(IBinder windowToken, int flags) { return hideSoftInputFromWindow(windowToken, flags, null); }共通化したらこんな形になりそう(おまけ)
共通化ファイル
objectで定義
KeyboardUtils.ktobject KeyboardUtils { fun hideKeyboard(focusView: View) { val inputMethodManager: InputMethodManager = focusView.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager inputMethodManager.hideSoftInputFromWindow( focusView.windowToken, InputMethodManager.HIDE_NOT_ALWAYS ) } }Activity
Activityで呼ぶ
MainActivity.kt// ~ 省略 ~ override fun onTouchEvent(event: MotionEvent?): Boolean { val focusView = currentFocus ?: return false KeyboardUtils.hideKeyboard(focusView) return false }補足
Activity
に実装していれば、関連するFragment
でも同様に動作するためFragment
には実装の必要なしスペシャルサンクス(参考にさせていただいた資料)
- https://developer.android.com/reference/android/view/inputmethod/InputMethodManager
- http://blog.lciel.jp/blog/2013/12/03/android-touch-event/
最後に
簡単な短い記事でしたが、誤り、ご指摘などあればコメントいただければ幸いです!
ありがとうございました!!
- 投稿日:2021-01-01T12:48:41+09:00
【Android】PreferenceFragmentCompatでTwitter風の設定画面を作る
PreferenceFragmentCompat、とても便利ですが痒いところに手が届きませんよね。特にTwitter系の設定レイアウトはよく見ますが、標準では用意されていません。
ここでは設定画面のカスタマイズ方法について説明していきたいと思います。
完成図 参考レイアウト(Twitter)
タイトルではTwitter風と言いましたがあまり似ていませんね実装
Android Studioに用意されているテンプレート
Setting Activity
を元に作っていきます。1. 土台となるコード
PreferenceFragmentCompatはxmlで背景色を指定する方法がありませんので、
setBackgroundColor
を使用して背景色を変更します。SettingsFragment.ktpackage com.sakusaku.test import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.preference.PreferenceFragmentCompat class SettingsFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.root_preferences, rootKey) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = super.onCreateView(inflater, container, savedInstanceState) // 背景色の設定 view?.setBackgroundColor(Color.parseColor("#F5F5F5")) return view } }次に設定画面のレイアウトを作成します。
res/layout/root_preferences.xml<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android"> <ListPreference android:defaultValue="online" android:entries="@array/entries_preference_status" android:entryValues="@array/values_preference_status" android:key="preference_status" android:layout="@layout/custom_preference" android:title="@string/preference_status" app:useSimpleSummaryProvider="true" /> <ListPreference android:defaultValue="short" android:entries="@array/entries_preference_scan_period" android:entryValues="@array/values_preference_scan_period" android:key="preference_scan_period" android:layout="@layout/custom_preference" android:title="@string/preference_scan_period" app:useSimpleSummaryProvider="true" /> <PreferenceScreen android:layout="@layout/preference_diver" android:selectable="false" /> <PreferenceScreen android:key="preference_notice" android:layout="@layout/custom_preference" android:title="@string/preference_notice" /> <PreferenceScreen android:key="preference_about" android:layout="@layout/custom_preference" android:title="@string/preference_about" /> <PreferenceScreen android:key="preference_send_feedback" android:layout="@layout/custom_preference" android:title="@string/preference_send_feedback" /> <PreferenceScreen android:key="preference_privacy_policy" android:layout="@layout/custom_preference" android:title="@string/preference_privacy_policy" /> <PreferenceScreen android:key="preference_oss_license" android:layout="@layout/custom_preference" android:title="@string/preference_oss_license" /> </PreferenceScreen>リソース等は下記の通りです。
res/values/string.xml<resources> <string name="preference_status">ステータスを設定</string> <string name="preference_scan_period">スキャンの間隔</string> <string name="preference_notice">お知らせ</string> <string name="preference_about">このアプリについて</string> <string name="preference_send_feedback">フィードバックを送る</string> <string name="preference_privacy_policy">プライバシーポリシー</string> <string name="preference_oss_license">オープンソースライセンス</string> </resources>res/values/array.xml<resources> <string-array name="entries_preference_status"> <item>オンライン</item> <item>オフライン</item> </string-array> <string-array name="values_preference_status"> <item>online</item> <item>offline</item> </string-array> <string-array name="entries_preference_scan_period"> <item>短い</item> <item>中間</item> <item>長い</item> </string-array> <string-array name="values_preference_scan_period"> <item>short</item> <item>middle</item> <item>long</item> </string-array> </resources>2.
PreferenceScreen
とListPreference
用のカスタムレイアウトを作成Preferenceには標準で透過色が設定されているため、白の背景色を設定したカスタムレイアウトを作成します。
元のレイアウトはAOSP上に公開されています。res/layout/custom_preference.xml<?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2014 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <!-- Layout for a Preference in a PreferenceActivity. The Preference is able to place a specific widget for its particular type in the "widget_frame" layout. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:baselineAligned="false" android:clipToPadding="false" android:foreground="?attr/selectableItemBackground" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeightSmall" android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> <LinearLayout android:id="@+android:id/icon_frame" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="-4dp" android:gravity="start|center_vertical" android:minWidth="60dp" android:orientation="horizontal" android:paddingStart="0dp" android:paddingTop="4dp" android:paddingEnd="12dp" android:paddingBottom="4dp"> <com.android.internal.widget.PreferenceImageView android:id="@+android:id/icon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxWidth="48dp" android:maxHeight="48dp" /> </LinearLayout> <RelativeLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:paddingTop="16dp" android:paddingBottom="16dp"> <TextView android:id="@+android:id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="marquee" android:singleLine="true" android:textAppearance="?android:attr/textAppearanceListItem" /> <TextView android:id="@+android:id/summary" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+android:id/title" android:layout_alignStart="@+android:id/title" android:ellipsize="end" android:maxLines="10" android:textAppearance="?android:attr/textAppearanceListItemSecondary" android:textColor="?android:attr/textColorSecondary" /> </RelativeLayout> <LinearLayout android:id="@+android:id/widget_frame" android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="end|center_vertical" android:orientation="vertical" android:paddingStart="16dp" android:paddingEnd="0dp" /> </LinearLayout>3. Preference同士の隙間(区切り)を表示
Preferenceの区切りごとに隙間を開けるカスタムレイアウトを作成します。
res/layout/preference_diver.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <View android:layout_width="match_parent" android:layout_height="16dp" /> </LinearLayout>表示の際は
PreferenceScreen
に上記のレイアウトを追加します。android:selectable="false"
を指定することでクリックを無効化出来ます。res/layout/custom_preference.xml<PreferenceScreen android:layout="@layout/preference_diver" android:selectable="false" />