20211012のAndroidに関する記事は3件です。

Flutterチュートリアル Todoアプリを作ろう! ~環境構築~

この記事を書こうと思った理由  この記事を書く前はFlutterでMarkdownで書けるメモアプリを作成していました。そのアプリの制作が終わったので、ここらへんで一旦自分のFlutterの知識をアウトプットしようと思いこの記事を書きました。 デバック環境 Windows 10 Android 30 Flutter環境構築 FlutterSDKのダウンロード  FlutterSDKのダウンロードリンク ここからSDKをダウンロードしCドライブ直下などに展開してください。 Flutter SDKのパス通し  SDKの展開したフォルダ・ディレクトリの名前が「flutter」になってると思います。その中の「bin」のファルダのパスを通してください。 Android Studioのインストール  Android Studioのダウンロードリンク ここからインストーラーをダウンロードしインストールしてください。 Flutter doctor  コマンドで下のコマンドを打つ。(※パスを通さないとコマンドが通らない) $ flutter doctor 大体はここで「Androidライセンスが*#$*%」みたいなやつが英語で出ることがあるのでつまずきますが、大丈夫です。 そんな時は、Android StudioのSDKマネージャーを開きAndroid SDK => SDK Tools => Android SDK Command-line Tools (latest)にチェックを入れ、右下の「apply」を押してから「ok」を押します。 その後、下のコマンドを打つと「Androidのライセンスを受け入れますか?」的なメッセージが英語で出るので、全部「y」を押しときます。 $ flutter doctor --android-licenses そのあともう一度しっかり「flutter doctor」を確認します。 最後にAndroid StudioにFlutterプラグインを入れる。これが終わればアプリ開発を始めることができます。 参考「Flutter大学」さんの動画を参考にしました。 初めてのFlutterデビュー  まず初めにAndroid Studioを開いて新しいFlutterのプロジェクトを作成します。 「Next」などを押して次に進むと、Flutter SDKのおいてある場所の登録をする画面が出てきます。 そこに環境構築で展開したflutterフォルダーのパスを入れます。 そうするとSDKのパスが登録されます。たまに「SDKがありません」などと出てきますが、パスが通ってないことや「Flutter doctor」をクリアしてないことがよくあります。 そこらへんは、気を付けてください。 Flutterのプロジェクト  Flutterのプロジェクトを作成するといろんなファイルが作成されていますね。 まず「lib」の中には、Flutterのプログラム本体が入っています。これからいじるのはここのプログラムです。 次に「ios」の中には、iosの設定ファイルやFlutterのプログラムをiosのプログラムに移植するためのコードが入っています。 最後に「android」の中には、iosと同じようにAndroidの設定ファイルなどが入っています。  主によくいじるのはここら辺のフォルダにあるファイルです。 そのほかにも「build」フォルダや「dart tool」などいろんなフォルダがあります。 Let`s run の前に  それでは、走らせてみましょう! とその前に、一番左上のFileのボタンを押してその中にある「Project structure」を開きます。そして、<No SDK>と書いてあるところをクリックしSDKを追加します。 追加前 追加後 そして次に、Project structureの左側にある「Modules」をクリックし、todo_appと書かれているところの上の「+」ボタンを押してAndroidのモジュールを追加します。 こうすることで、Android Studioについている機能でエミュレーター(PC上でスマホのOSを実行しアプリをデバックできるもの)の中のファイルを確認出る「Device File Explorer」が使えるようになります。 ここでAndroidのモジュールを追加すると、左側に表示されているファイル・フォルダのリスト(以後エクスプローラーと呼ぶ)の表示が少なくなっていると思います。  これはモジュールを追加したときに、このプロジェクトがAndroidのプロジェクトとして開かれている状態なので普通のFlutterのプロジェクトとして開く必要があります。下の写真のAndroidのところをProjectに変更しておきましょう。 次にアプリを実行する環境を作ります。 それではそのエミュレーターを作っていきましょう。  上の画像のボタンを押します。(AVD Manager) そうすると、AVDのリストが出てきますが初めての場合は何もAVDが無いので作成しましょう。 AVD Managerの下の方にある「Create Virtual Device」を押します。そうするとスマホの機種を選ぶ画面が出てきます。 自分の好きな機種を選びましょう。ただしPlay Storeのところにマークがついているものを選びましょう。(Play Storが必要ならばですが。) 次に進むとAndroidのシステムイメージを選ぶところに来ます。初めての人は使えるAndroidのバージョンが無いと思うので、Recommendedから好きなバージョンのAndroidをダウンロードしてから選択するようにしましょう。 次にAVDの名前を決めるところに来ます。わかりやすい名前を付けましょう。 最後にFinishを押すと、最初のAVDのリスト画面に一つエミュレーターができていることが分かります。 緑のスタートボタンを押してスタートするとAndroidの既視感のある画面が出てきます。 Let`s run !  それでは今度こそほんとに走らせてみましょう。 この写真の左の方に、「emulator-5554(mobile)」と書いてありますが、そこがWebブラウザになっていることがあります。 しっかりとmobileが選択されていることを確認しましょう。 FlutterはWebアプリの開発も出来ます。知らんけど... しっかりmobileを選択しないと勝手にブラウザで実行されます。 そしたら、スタートボタンを押しましょう!初めてのFlutterアプリが動き出します。 初めてのアプリはカウントアプリが立ち上がると思います。遊んでください。 これで環境構築は終了です。 この記事のまとめ mobileの開発はわからないことがいっぱいで、さらにFlutterの環境構築もイミフな奴が多いのでここでしっかりと順番道理に行ってください。 多分成功すると思います。多分!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Roomの実装方法まとめ

前々回、前回に続き、今回はRoomの実装方法をまとめてみようと思います。 この記事の内容 公式のドキュメントやトレーニング「Android Kotlin の基礎」のレッスン6「Room データベースとコルーチン」を参考に実装のポイントとエッセンスをまとめていきます。 前提知識 kotlinの基礎的な文法 AndroidStudioの使い方/アプリの作り方 画面や画面部品の配置方法 Navigationの実装方法(前々回記事参照) ViewModelの実装方法(前回記事参照) 開発環境 Windows 10 Home Android Studio 4.2.1 作成するサンプル 簡単な単語帳アプリを作成します。 Createボタンをクリックすると画面上のListViewに単語の一覧が表示され、Saveボタンのクリックにより内容をローカルDBに保存します。 ローカルDBにデータが保存済みの場合はLoadボタンによりデータを読み込む機能を実装します。 DeleteボタンがクリックされたらローカルDBのデータを消去します。 この操作イメージだと少し分かりにくいかもしれませんが、LoadボタンをクリックしたときRoomDBからデータを取得し、その際オーダーを掛けているので画面上のデータの並びも変わっています。 前提となるコード 前述の通りこの記事ではNavigation、ViewModelについては既知のものとし、詳細は扱いません(詳しくは前回までの記事に書いているので、良かったら見てみてね)。 ただちょっとハンズオンっぽく進めていきたいので、前提となるコードを以下に記します。 また今回はListViewをViewModel、LiveData、DataBindingにより実装していますが、以下の記事を参考にさせていただきました(大変分かりやすく、参考になりました)。 【Android】LiveData+DataBinding+ViewModelでListView作成 コード上、自分の記事で触れていない部分はコメントを残しています。 動作イメージはこんな感じ。 Saveボタン、LoadボタンはRoomの領域なのでこの時点では未実装です。 またDeleteボタンは画面上の表示を削除するまでの実装です。 Navigation、ViewModel、DataBinding用にgradleファイルを設定しておけばとりあえず上記のイメージ通りのアプリにはなります。 レイアウトファイル activity_main.xml <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <fragment android:id="@+id/myNavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment" app:navGraph="@navigation/navigation" app:defaultNavHost="true" android:layout_width="match_parent" android:layout_height="match_parent"/> </androidx.constraintlayout.widget.ConstraintLayout> fragment_first.xml アプリ起動時の画面です。 <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="startViewModel" type="com.warpstudio.android.roomsample.StartViewModel" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".StartFragment"> <TextView android:id="@+id/startText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Welcome to Room Sample APP!" android:textSize="24sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/toSecondFragmentButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="To SecondFragment" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/startText" android:onClick="@{() -> startViewModel.onToSecondFragment()}"/> </androidx.constraintlayout.widget.ConstraintLayout> </layout> fragment_second.xml 遷移先の画面。 <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="secondViewModel" type="com.warpstudio.android.roomsample.SecondViewModel" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".SecondFragment"> <Button android:id="@+id/createButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Create" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/saveButton" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" tools:layout_editor_absoluteY="0dp" android:onClick="@{()->secondViewModel.creteData()}"/> <Button android:id="@+id/saveButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Save" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/loadButton" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@+id/createButton" tools:layout_editor_absoluteY="0dp" android:onClick="@{()->secondViewModel.saveData()}"/> <Button android:id="@+id/loadButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Load" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/deleteButton" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@+id/saveButton" tools:layout_editor_absoluteY="0dp" android:onClick="@{()->secondViewModel.loadData()}"/> <Button android:id="@+id/deleteButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Delete" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@+id/loadButton" tools:layout_editor_absoluteY="0dp" android:onClick="@{()->secondViewModel.deleteData()}"/> </androidx.constraintlayout.widget.ConstraintLayout> <ListView android:id="@+id/wordList" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout> </layout> list_item.xml ListViewに表示する行単位のレイアウトです。 <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="wordData" type="com.warpstudio.android.roomsample.WordData" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <TextView android:text="@{wordData.word}" android:textSize="18sp" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="0.8"/> <TextView android:text="@{wordData.speech}" android:textSize="18sp" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="0.2"/> <TextView android:text="@{wordData.meaning}" android:textSize="18sp" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1"/> </LinearLayout> </layout> navigation.xml Navigationファイルも一応載せときます。 <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/navigation" xmlns:tools="http://schemas.android.com/tools" app:startDestination="@id/startFragment"> <fragment android:id="@+id/startFragment" android:name="com.warpstudio.android.roomsample.StartFragment" android:label="StartFragment" tools:layout="@layout/fragment_start"> <action android:id="@+id/action_startFragment_to_secondFragment" app:destination="@id/secondFragment" /> </fragment> <fragment android:id="@+id/secondFragment" android:name="com.warpstudio.android.roomsample.SecondFragment" android:label="SecondFragment" tools:layout="@layout/fragment_second"/> </navigation> Fragmentクラス MainActivity.xml は何もいじっていないので省略。 StartActivity.kt ViewModel使ってますが、次の画面に映るボタンがあるだけなんでわざわざそこまでする必要はないんですけどね……。 class StartFragment : Fragment() { private lateinit var binding: FragmentStartBinding private lateinit var viewModel: StartViewModel private lateinit var viewModelFactory: StartViewModelFactory override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = DataBindingUtil.inflate( inflater, R.layout.fragment_start, container, false ) viewModelFactory = StartViewModelFactory() viewModel = ViewModelProvider(this, viewModelFactory) .get(StartViewModel::class.java) viewModel.eventToSecondFragment .observe(viewLifecycleOwner, Observer { isToSecond -> if (isToSecond) { toSecondFragment() } }) binding.startViewModel = viewModel return binding.root } private fun toSecondFragment() { val action = StartFragmentDirections .actionStartFragmentToSecondFragment() NavHostFragment.findNavController(this).navigate(action) viewModel.onToSecondFragmentComplete() } } StartViewModel.kt class StartViewModel : ViewModel() { private val _eventToSecondFragment = MutableLiveData<Boolean>() val eventToSecondFragment: LiveData<Boolean> get() = _eventToSecondFragment init { } fun onToSecondFragment() { _eventToSecondFragment.value = true } fun onToSecondFragmentComplete() { _eventToSecondFragment.value = false } } StartViewModelFactory.kt class StartViewModelFactory: ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>): T { if (modelClass.isAssignableFrom(StartViewModel::class.java)) { return StartViewModel() as T } throw IllegalArgumentException("ERR!") } } SecondFragment.kt メインの画面です。 ListViewにAdapterを設定しています。 class SecondFragment : Fragment() { private lateinit var binding: FragmentSecondBinding private lateinit var secondViewModel: SecondViewModel private lateinit var secondViewModelFactory: SecondViewModelFactory override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = DataBindingUtil.inflate( inflater, R.layout.fragment_second, container, false ) secondViewModelFactory = SecondViewModelFactory() secondViewModel = ViewModelProvider(this, secondViewModelFactory) .get(SecondViewModel::class.java) // wordListのObserverを登録 secondViewModel.wordList.observe(viewLifecycleOwner, Observer { words -> // 作成されたWordListをもとにListViewのデータを更新 binding.wordList.adapter = WordAdapter(words) }) binding.secondViewModel = secondViewModel binding.lifecycleOwner = viewLifecycleOwner // ListViewのAdapterを初期化 binding.wordList.adapter = WordAdapter(ArrayList(0)) return binding.root } } SecondViewModel.kt 主役(?)となるViewModelクラス。 class SecondViewModel: ViewModel() { private val _wordList = MutableLiveData<List<WordData>>() val wordList: MutableLiveData<List<WordData>> get() = _wordList init { } // Createボタンクリック時のイベント fun creteData() { wordList.value = createWordList() } // Saveボタンクリック時のイベント fun saveData() { } // Loadボタンクリック時のイベント fun loadData() { } // Deleteボタンクリック時のイベント fun deleteData(){ wordList.value = ArrayList(0) } private fun createWordList() : ArrayList<WordData> { var data = arrayListOf( WordData("ethos", "名", "気風"), WordData("mediocre", "形", "平凡な"), // (略) WordData("lurch", "動", "よろめく") ) return data } } SecondViewModelFactory.kt コピペでOKなViewModelFactoryクラス。 class SecondViewModelFactory: ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>): T { if (modelClass.isAssignableFrom(SecondViewModel::class.java)) { return SecondViewModel() as T } throw IllegalArgumentException("ERR!") } } データ/Adapterクラス カスタマイズしたListViewにDataBindingでデータを表示するために実装するクラスです。 WordData.kt 単語の単語名、品詞、意味を保持するデータクラス。 data class WordData ( var word: String, var speech: String, var meaning: String ) WordAdapter.kt ListView用のAdapterクラスです。 // Adapterクラス class WordAdapter(private var wordDatas: List<WordData>): BaseAdapter() { // WordAdapterクラス生成時に呼び出されるメソッド override fun getView( posistion: Int, convertView: View?, parent: ViewGroup ): View { val binding = if (convertView == null) { ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) } else { DataBindingUtil.getBinding(convertView) ?: throw IllegalStateException() } with(binding) { wordData = wordDatas[posistion] executePendingBindings() } return binding.root } // その他Overrideが必要なメソッド override fun getItem(position: Int) = wordDatas[position] override fun getItemId(position: Int) = position.toLong() override fun getCount() = wordDatas.size } Gradleファイルの更新 作業に入る前にgradleファイルを更新していきます。 Roomはこの辺りに起因するビルドエラーが出やすいので注意してください。 build.gradel(app)ファイルの更新 Roomをか使う場合はkaptプラグインとdependenciesブロックに下記の内容を追加してください。 plugins { // (略) id 'kotlin-kapt' } dependencies { // (略) // Room implementation "androidx.room:room-runtime:2.3.0" kapt "androidx.room:room-compiler:2.3.0" implementation "androidx.room:room-ktx:2.3.0" testImplementation "androidx.room:room-testing:2.3.0" } DBの実装 それではDBを実装していきます。 RoomにはEntity、DAO、Databaseの3要素が必要なので、まずはそれらのクラスを実装していきます。 Entityクラスの作成 新規 Kotlin Class を作成し、data classを定義します。 EntityクラスはDBのテーブルに該当し、各の名前と型、主キーの情報を保持します。 あわせてクラスには@Entityアノテーションとテーブル名の設定が必要です。 @Entity(tableName = "word_data_table") data class WordEntity ( @PrimaryKey(autoGenerate = true) var wordId: Long = 0L, @ColumnInfo(name = "word") var word: String, @ColumnInfo(name = "speech") var speech: String, @ColumnInfo(name = "meaning") var meaning: String ) 上記では主キーのautoGenerate属性をtrueとしていますが、これによりCREATE等の際に主キーは自動生成されます。 DAOクラスの作成 DAO用に新規 Kotlin Class を作成します。 RoomにおけるDAOはDBに対するCREATE、INSERT、DELETE等の操作を定義するクラスといったところでしょうか。 DAOはinterfaceである必要があります。 また、DBにはコルーチンを使用してアクセスするため、非同期で実行するメソッドに対してsuspendキーワードを設定しておきます。 @Dao interface WordDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(word: List<WordEntity>) @Delete suspend fun delete(word: List<WordEntity>) @Query("SELECT * FROM word_data_table ORDER BY wordId DESC") suspend fun getAllWord(): List<WordEntity> } 基本的には@Queryアノテーションを設定したメソッドに自分でSQL文を書く必要がありますが、INSERTやUPDATE等予め定義された操作に関してはプリセットのアノテーションを利用することもできます。 Databaseクラスの作成 最後にDatabaseクラス(DBホルダークラスト言った方が正しいかな?)を作成します。 DBインスタンスがnullの場合にインスタンスを生成するgetInstanceメソッドを作成します。 @Database(entities = [WordEntity::class], version = 1, exportSchema = false) abstract class WordDatabase: RoomDatabase() { abstract val wordDao: WordDao companion object { @Volatile private var INSTANCE: WordDatabase? = null fun getInstance(context: Context): WordDatabase { synchronized(this) { var instance = INSTANCE if (instance == null) { instance = Room.databaseBuilder( context.applicationContext, WordDatabase::class.java, "database" ) .fallbackToDestructiveMigration() .build() INSTANCE = instance } return instance } } } } DBへのアクセス 前々章で実装したDBにViewModelからアクセスします。 ViewModel、ViewModelFactoryの改修 ViewModel、ViewModelFactoryについて、引数にDAOとApplicationを受け取るよう改修します。 下記のコードではAndroidViewModelを継承元としています。これはViewModelと基本的には同じですが、コンストラクタのパラメーターとしてアプリケーションのContextを受け取ることができます。 ViewModel class SecondViewModel( val database: WordDao, application: Application): AndroidViewModel(application) { // 略 } ViewModelFactory class SecondViewModelFactory( private val dataSource: WordDao, private val application: Application): ViewModelProvider.Factory { @Suppress("unchecked_cast") override fun <T : ViewModel?> create(modelClass: Class<T>): T { if (modelClass.isAssignableFrom(SecondViewModel::class.java)) { return SecondViewModel(dataSource, application) as T } throw IllegalArgumentException("ERR!") } } Fragment UI Controllerの(今回はFragment)onCreate()メソッドでViewModelを生成します。 前述の通りViewModelのコンストラクタにはDAOのインスタンスとアプリケーションのContextが必要なため、個別に作成しておきます。 override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // (略) // アプリケーションの生成 val application = requireNotNull(this.activity).application // DAOインスタンスの生成 val dataSource = WordDatabase.getInstance(application).wordDao // ViewModelFactoryの生成 secondViewModelFactory = SecondViewModelFactory(dataSource, application) // ViewModelFactoryの生成 secondViewModel = ViewModelProvider(this, secondViewModelFactory) .get(SecondViewModel::class.java) // (略) } 各処理の実装 ViewModelにDBにアクセスする処理を追記していきます。 DBへのアクセスはすべてコルーチンスコープ内で処理する点に注意してください。 ViewModel class SleepTrackerViewModel( val database: SleepDatabaseDao, application: Application) : AndroidViewModel(application) { // (略) // Createボタンクリック時のイベント fun creteData() { _wordList.value = createWordList() } // Saveボタンクリック時のイベント fun saveData() { // Createが実行され、LiveDataに値が設定されている場合のみ処理を実行 if (!_wordList.value.isNullOrEmpty()) { // DataをEntityのListに置き換え val words = _wordList.value!!.map { WordEntity(word = it.word, speech = it.speech, meaning = it.meaning) } as ArrayList<WordEntity> // 保存処理はコルーチンのスコープ内で実行 viewModelScope.launch { database.insert(words) } } } // Loadボタンクリック時のイベント fun loadData() { // コルーチンのスコープ内で実行 viewModelScope.launch { // DBから全レコードを取得 val allWords = database.getAllWord() // EntityをDataのListに置き換え val wordList = allWords.map { WordData(word = it.word, speech = it.speech, meaning = it.meaning) } as ArrayList<WordData> // LiveDataにListを設定 _wordList.value = wordList } } // Deleteボタンクリック時のイベント fun deleteData(){ // LiveDataの削除処理 if (!_wordList.value.isNullOrEmpty()) { _wordList.value = ArrayList(0) } // DBの処理 // コルーチンのスコープ内で処理 viewModelScope.launch { val allWords = database.getAllWord() database.delete(allWords) } } // (略) } Recap 今回は以上です。 Roomはそれ自体は非常に便利なのですが細かなビルドエラーが出やすく、それがちょっと厄介ですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React Native】FCMの設定でつまずいたこと(Androidのみ)

1、FCM(Firebase Cloud Messaging)とは Firebaseが提供しているアプリにプッシュ通知を送る機能です。 機能自体無料で使えて、アプリ側ではfirebaseから取得したfcmトークンをfcmに設定するだけでプッシュ通知を受け取れるためかなり便利です。 本格的なアプリを作るとなるとプッシュ通知は必要不可欠です。 https://firebase.google.com/docs/cloud-messaging?hl=ja 2、FCMの簡単な仕組み・設定方法 https://firebase.google.com/?hl=ja ↑Firebaseでプロジェクトを作成し、左のメニュー欄からCloud Messagingを選択します 開始方法からiOS、Android両方の設定をします。 iOSの方はAPNs認証キーの設定も必要なため、Apple Developerの登録も必要です。 今回はAndroidのみですのでそこは割愛します。   https://rnfirebase.io/messaging/usage 次にアプリ側でFCMトークンを取得し、そのFCMごとに識別して通知が送られてきます。 本題 プロジェクトでFCMを使うことになり、アプリの管理画面から通知文を作成し、APIからFCMにリクエストを出してFCMを経由してアプリに通知が届くような仕様でした。 今回FCMの実装をしている時ににつまずいたことは ・FCMの画面から直接トークンを指定して、送った場合→通知が届き、タップするとアプリが起動する ・アプリの管理画面から通知を送信し、APIからリクエストを出してFCMを経由して送った場合→通知は届くがタップしてもアプリが起動しない という件でした。 なぜか管理画面から送る通知はタップしても起動することなく、そのまま通知メッセージが消えるだけでした。 原因 FCMから直接送った通知のデータと管理画面でAPIを使って送った通知のデータを見比べてみたところ原因が判明しました。 FCMから返ってくるデータを取得するにはsetBackgroundMessageHandlerを使います。 アプリがバックグラウンドにある状態もしくはキルしている状態の時、プッシュ通知が送られてきた時にFCMからのレスポンスを取得することができます。 以下のコードをindex.jsに追記するとremoteMessageにデータが入っています。 import messaging from '@react-native-firebase/messaging'; messaging().setBackgroundMessageHandler(async remoteMessage => { console.log('Message handled in the background!', remoteMessage); }); ちなみにフォアグラウンド状態でも通知のデータを受け取れますが今回は割愛します。 データを比べてみる 1、firebaseでタイトルとテキストを入力、fcm通知を選んで送信 {  "notification": {  "android": {},  "body": "通知本文",  "title": "通知タイトル"  },  "from": number,  "ttl":number,  "sentTime": 1633065375430,  "data": {}, "messageId": "${messageId}", "collapseKey": "バンドルID" } 2、管理画面(API)から通知を送信 {  "notification": {  "android": {   "clickAction": "attention"  },  "body": "通知本文" },  "sentTime": 1633065600374,  "data": {},  "from": "501097458848",  "messageId": "${messageId}",  "ttl": 2419200,  "collapseKey": "バンドルID" } 管理画面から送った方にはandroidオブジェクト内にclickActionが入っています。 これが原因でした。 https://firebase.google.com/docs/cloud-messaging/http-server-ref?hl=ja ここのclick_actionのところに記載されているとおり、 ユーザーが通知をクリックしたときに実行されるアクション。 これが指定されている場合、ユーザーが通知をクリックすると、一致するインテント フィルタを持つアクティビティが起動します。 clickActionを指定するとfcmトークンに紐ずくアプリを起動するのではなく、一致するインテントフィルタ(アプリで別途設定する値)に紐ずくアプリを起動させる挙動になります。 今回このインテントフィルタを設定していなかったので一致するインテントフィルタがなく、アプリが起動しませんでした。 ちなみにこのclickActionの使い道ですが、 例えば 1、LINEアプリからプロフィール欄のBGMを設定しませんか?と通知がくる 2、ユーザーがタップするとLINEアプリではなく、LINE MUSICアプリが起動し、すぐ設定が行える こんな挙動をしたい時は便利かもしれません。 ただ別のアプリを起動させる必要もないため、今回はclickActionは削除する方針にしました。 clickActionを削除すると問題なくアプリが起動しました! 補足 アプリ起動後、通知があるページ(お知らせ画面)に遷移させたいときは 1、アプリがバックグラウンド状態の時:onNotificationOpenedApp 2、アプリを閉じている状態の時:getInitialNotification この二つを初期マウント時(useEffect)に走らせればデータを取得できます。 そのデータから通知のidを受け取りそれに紐ずく画面に遷移すれば意図した挙動になるかと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む