- 投稿日:2020-05-25T21:12:45+09:00
RecyclerViewとLiveDataを使ってリスト表示
はじめに
今回は、前回の続きをやって聞きたいと思います。前回は
RecyclerView
を使ってダミーのデータをリスト表示することができました。今回はEditText
で入力したデータをリスト表示できるようします。目標
環境
- Android Studio 3.6.3
- Kotlin 1.3.72
作成手順
Gradle
build.gradledependencies { // 省略 implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.1.0'実装
レイアウトの作成
activity_main.xmlにEditTextとButtonを配置します。
activity_main.xml<?xml version="1.0" encoding="utf-8"?> <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"> <EditText android:id="@+id/submit_text" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="10dp" android:layout_marginTop="45dp" android:autofillHints="@string/input" android:hint="@string/input" android:imeOptions="actionSend" android:inputType="text" app:layout_constraintBottom_toTopOf="@id/main_recycler_view" app:layout_constraintEnd_toStartOf="@+id/button" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/main_recycler_view" android:layout_width="0dp" android:layout_height="0dp" android:scrollbars="vertical" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/submit_text" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="4dp" android:text="@string/button" app:layout_constraintBottom_toTopOf="@+id/main_recycler_view" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/submit_text" /> </androidx.constraintlayout.widget.ConstraintLayout>MainViewModelの作成
EditText
から受け取ったデータをRecyclerView
に反映させるためにLiveData
を使用する。LiveData
はデータの更新を監視します。今回はLiveData
を使用しなくても直接Adapter
に値をセットしてあげれば反映できるが、今後データベースの値を非同期で反映させるためにはLiveData
を使用します。MainViewModel.ktclass MainViewModel : ViewModel() { val todo = MutableLiveData<String>() fun addItem(item: String) { todo.value = item } }MainActivityの定義
ボタンが押された時にEditTextの値を
MainViewModel
の変数todo
に代入します。その値の変化を監視してAdapter
にItem
をセットする。MainActivity.ktclass MainActivity : AppCompatActivity() { private val mainViewModel = MainViewModel() lateinit var adapter: RecyclerAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) adapter = RecyclerAdapter() main_recycler_view.layoutManager = LinearLayoutManager(this) main_recycler_view.adapter = adapter main_recycler_view.setHasFixedSize(true) add_item_button.setOnClickListener { mainViewModel.addItem(submit_text.text.toString()) } mainViewModel.todo.observe(this, Observer { adapter.setItem(it) }) } }Adapterの定義
MainActivity
から呼び出されたsetItem(item: String)
によってAdapter
に値をセットします。notifyDataSetChanged()
を書かないと最新の状態に反映されないので注意。RecyclerAdapter.ktclass RecyclerAdapter : RecyclerView.Adapter<RecyclerAdapter.RecyclerViewHolder>(){ private val todoList = mutableListOf<String>() fun setItem(item: String) { todoList.add(item) notifyDataSetChanged() } class RecyclerViewHolder(val view: View): RecyclerView.ViewHolder(view) { val sampleImg = view.sampleImg val sampleTxt = view.sampleTxt } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder { val layoutInflater = LayoutInflater.from(parent.context) val item = layoutInflater.inflate(R.layout.recyclerview_item, parent, false) return RecyclerViewHolder(item) } override fun getItemCount(): Int = todoList.size override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) { holder.view.let{ it.sampleImg.setImageResource(R.mipmap.ic_launcher_round) it.sampleTxt.text = todoList[position] } } }まとめ
今回は
EditText
の値をRecyclerView
に反映させましたが、このままだとアプリを終了した時にデータが残らないのでRoomを使ってデータベースに値を保存するなどしなければならない。
次回はRoom
を使ってデータベースに値を保存する方法について書きます!
- 投稿日:2020-05-25T17:56:34+09:00
Android Studio activity_main.xmlでXMLではなくDesign Editorを使う方法
はじめに
activity_main.xml
を開くと、デフォルトでは視覚的に操作するDesign Editorが開くかと。ただし、たまにXML
の表示になっていることがある。
Android Studioを使い慣れていない方は、ここで戸惑うことがたまにある。。そんな時のための記事
Design Editorに変更する方法
これでOK!
参考
https://developer.android.com/studio/write/layout-editor?hl=ja
- 投稿日:2020-05-25T13:48:59+09:00
Androidのプッシュ通知アイコンの設定 -Cordova
Androidのプッシュ通知(Firebase Cloud Messaging)のアイコン設定でググってもうまくいかないところがあったのでまとめます。
本記事ではcordovaの設定ファイル
config.xml
にアイコン画像とカラーの指定を行うことで、Androidの設定ファイルであるAndroidManifest
を書き換えていきます。結果としてAndroidビルドの実行で下のような
AndroidManifest.xml
が生成できていればOKです。AndroidManifest.xml<manifest> <application> <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/fcm_push_icon" /> <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/colorAccent" />アイコン画像のフォルダ構成はfirebassのAndroidプロジェクトが参考になります。
firebase quickstart-android -GitHub
本記事ではこのようなAndroidプロジェクトを生成することを目指しています。動作がうまくいかないときは、生成したプロジェクトと上のプロジェクトを比較してみてください。プッシュ通知用のアイコンの設定
プッシュ通知用のアイコンを設定しておくと通知が来た際にステータスバーにアイコンとして表示します。画像にアルファチャネルがないと真っ白のアイコンになってしまいます。
アイコンの見え方について下の記事が参考になりました。
Firebase Notificationsのアイコンについて纏めてみた -Qiitaldpi〜xxxhdpiの各サイズのアイコン画像を用意します。
私は1024×1024の画像を用意しionic cordova resourcesを使って生成しました。各サイズのアイコン画像を配置し、
res
配下にコピーされるようにconfig.xml
設定します。config.xml<platform name="android"> <resource-file src="resources/android/fcm_push_icon/drawable-ldpi-icon.png" target="app/src/main/res/drawable-ldpi/fcm_push_icon.png" /> <resource-file src="resources/android/fcm_push_icon/drawable-mdpi-icon.png" target="app/src/main/res/drawable-mdpi/fcm_push_icon.png" /> <resource-file src="resources/android/fcm_push_icon/drawable-hdpi-icon.png" target="app/src/main/res/drawable-hdpi/fcm_push_icon.png" /> <resource-file src="resources/android/fcm_push_icon/drawable-xhdpi-icon.png" target="app/src/main/res/drawable-xhdpi/fcm_push_icon.png" /> <resource-file src="resources/android/fcm_push_icon/drawable-xxhdpi-icon.png" target="app/src/main/res/drawable-xxhdpi/fcm_push_icon.png" /> <resource-file src="resources/android/fcm_push_icon/drawable-xxxhdpi-icon.png" target="app/src/main/res/drawable-xxxhdpi/fcm_push_icon.png" /> </platform>次に通知アイコンを参照するように設定する記述を行います。
android:resource
部分にアイコン画像の拡張子を除いたファイル名と一致させます。config.xml<platform name="android"> <config-file parent="/manifest/application" target="AndroidManifest.xml"> <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/fcm_push_icon" /> </config-file> </platform>背景カラー設定
notification_color
を設定します。
カラーコードを記述するcolors.xml
の場所を指定します。config.xml<config-file parent="/manifest/application" target="AndroidManifest.xml"> <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/primary" /> </config-file>colors.xmlが生成されない場合
ビルドで
colors.xml
が生成されない場合はファイルを作成してAndroidプロジェクトにコピーするように設定します。config.xml<platform name="android"> ... <resource-file src="resources/android/res/values/colors.xml" target="app/src/main/res/values/colors.xml"/> </platform>colors.xml<?xml version="1.0" encoding="utf-8"?> <resources> <color name="primary">#FFFFFF00</color> <color name="primary_dark">#FF220022</color> <color name="accent">#FF00FFFF</color> </resources>ちょっとハマったこと
default_notification_color
が重複作成されてビルドエラーになることがありました。
cordova-plugin-firebasex
がカラー関連の定義を行なっているようで、重複してしまっていました。
カラー定義の記述を削除してプラグインオプションを変更することでカラー定義を行なっています。package.json"cordova-plugin-firebasex": { "ANDROID_ICON_ACCENT": "#7ECB5C" }参考にしたもの
- 投稿日:2020-05-25T07:12:39+09:00
FlutterでWebRTCをやってみる with AgoraSDK
はじめに
WebRTC
を使ったアプリを開発する機会があり、何かお手軽なpackage
ないかなーと探していたところFlutter
でWebRTC
を手軽に利用できるplugin
を見つけたので試しにアプリを作成してみました。WebRTC とは
WebRTC
(Web Real-Time Communication) とは、ビデオや音声、データをブラウザ間でやり取り可能にするための規格で、API
を経由することでリアルタイム通信を実現できます。最近ではコロナの影響もあり、ウェブ会議システムやチャットツールなどの利用者が急増しています。
- Zoom
- Hang out
- Discord
- Microsoft Teams
コロナをきっかけに一般に広く知られるようになり、今まで利用するに至らなかった勢が利用していました(周りでも)。
今後これらのツールが一般的に利用されるようになるのではないでしょうか。Agora.IO SDK
Agora.IO
が開発している、ビデオ通話やライブ配信を構築できるSDK
です。
基盤となるこのSDK
を利用して様々な言語やプラットフォームで利用することができます。
日本ではNTT Communications
のSkyWay
に相当するものです。
Flutter
では agora_rtc_engine | Flutter package を利用します。実装
実際にサンプルアプリを実装していきます.
Agora Project
の作成
Agora
を利用するにはAgora.IO
にProject
を作成し、AppID
を入手する必要があります.
無料枠が10000 minutes
程あるので当分は無料で利用できますし、無料枠を超えても金額を請求されることはないので安心してください.まずはこちらから登録して
Project
を作成します.
Agora | Sign Up
Project
を作成後、Project Management
をクリックして作成したProject
のAppID
をメモしておきます.Platform settings
Platform
毎に権限周りや固有の設定をしていきます.
iOS
ios/Runner/info.plist
にカメラとマイクの権限を追加info.plist<key>NSCameraUsageDescription</key> <string>Use camera</string> <key>NSMicrophoneUsageDescription</key> <string>Use mic</string> <key>UIBackgroundModes</key> <array> <string>audio</string> </array>
WebView
を利用するので以下も追加info.plist<key>io.flutter.embedded_views_preview</key> <true/>
Android
android/app/src/main/AndroidManifest.xml
に以下の権限を追加します.AndroidManifest.xml<uses-permission android:name="android.permission.READ_PHONE_STATE"/> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
android/app/src/proguard-rules.pro
を作成し、以下を書いておきます
(難読化によるアプリクラッシュを防ぐ).proguard-rules.pro-keep class io.agora.**{*;}
platform
固有の設定は以上です.
agora_rtc_engine
周りの実装ここがメインの部分になります.
agora_rtc_engine
の追加pubspeck.yamldependencies: agora_rtc_engine: ^1.0.12Controller
今回は
state_notifier
とfreezed
パッケージを利用して実装しました。
agora_rtc_engine
に関する処理は全部この中でやっています。WebRtcController.dartimport 'package:agora_example/models/entities/entities.dart'; import 'package:agora_example/utils/constants.dart'; import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:state_notifier/state_notifier.dart'; import 'webrtc_state.dart'; class WebRtcController extends StateNotifier<WebRtcState> { WebRtcController({ @required String roomName, }) : super(const WebRtcState()) { debugPrint('$tag: init()'); initWebRtc(roomName: roomName); } static const tag = 'WebRtcController'; Future<void> initWebRtc({@required String roomName}) async { /// initialize する前に必ず Permission Request を行う await Permission.camera.request(); await Permission.microphone.request(); /// 先程メモした AppID を利用 await AgoraRtcEngine.create(Constants.appId); /// AV 周りに関する設定 await AgoraRtcEngine.enableAudio(); await AgoraRtcEngine.enableVideo(); await AgoraRtcEngine.setChannelProfile(ChannelProfile.Communication); await AgoraRtcEngine.enableWebSdkInteroperability(true); /// Event Listener を設定する /// 自分が Join に成功した時 AgoraRtcEngine.onJoinChannelSuccess = _onJoinChannelSuccess; /// 相手が Join に成功した時 AgoraRtcEngine.onUserJoined = _onUserJoined; /// 自分が Leave した時 AgoraRtcEngine.onLeaveChannel = _onLeaveChannel; /// 相手が Offline になった時 AgoraRtcEngine.onUserOffline = _onUserOffline; /// Join する処理 await AgoraRtcEngine.startPreview(); await AgoraRtcEngine.joinChannel(null, roomName, null, 0); } Future<void> toggleLocalAudio() async { final localAvStatus = state.localAvStatus; await AgoraRtcEngine.muteLocalAudioStream(localAvStatus.mic); state = state.copyWith( localAvStatus: localAvStatus.copyWith( mic: !localAvStatus.mic, ), ); } Future<void> toggleLocalVideo() async { final localAvStatus = state.localAvStatus; await AgoraRtcEngine.muteLocalVideoStream(localAvStatus.video); state = state.copyWith( localAvStatus: localAvStatus.copyWith( video: !localAvStatus.video, ), ); } void switchView(int viewIndex) { state = state.copyWith(viewIndex: viewIndex); } void _onJoinChannelSuccess(String roomName, int uid, int elapsed) { debugPrint('$tag: onJoinChannelSuccess -> $uid'); final users = [...state.users, WebRtcUser(uid: uid)]; state = state.copyWith( users: users, ); } void _onUserJoined(int uid, int elapsed) { debugPrint('$tag: onUserJoined -> $uid'); final users = [...state.users, WebRtcUser(uid: uid)]; state = state.copyWith( users: users, ); } void _onLeaveChannel() { debugPrint('$tag: onLeaveChannel'); state = state.copyWith(users: []); } void _onUserOffline(int uid, int reason) { debugPrint('$tag: onUserOffline -> $uid'); final users = <WebRtcUser>[]; for (final user in state.users) { if (user.uid != uid) { users.add(user); } } state = state.copyWith(users: users, viewIndex: 0); } @override void dispose() { super.dispose(); /// agora_rtc_engine の破棄 AgoraRtcEngine.leaveChannel(); AgoraRtcEngine.stopPreview(); AgoraRtcEngine.destroy(); } }View
Agora
からはPlatformView
が提供されるのでそれを利用します。webrtc_view.dartAgoraRenderWidget( uid, /// Join に成功した場合に取得できる uid local: true, /// 自分であれば true, それ以外は false preview: true, mode: VideoRenderMode.Hidden /// object-fit か object-cover か )Page
webrtc_page.dartimport 'package:agora_example/pages/webrtc_page/room_user_list.dart'; import 'package:agora_example/pages/webrtc_page/webrtc_view.dart'; import 'package:agora_example/pages/webrtc_page/call_action_button.dart'; import 'package:agora_example/models/models.dart'; import 'package:flutter_state_notifier/flutter_state_notifier.dart'; import 'package:provider/provider.dart'; import 'package:flutter/material.dart'; class WebRtcPage extends StatelessWidget { const WebRtcPage._({Key key}) : super(key: key); static Widget wrapped({@required String roomName}) { return MultiProvider( providers: [ StateNotifierProvider<WebRtcController, WebRtcState>( create: (context) => WebRtcController(roomName: roomName), ), ], child: const WebRtcPage._(), ); } @override Widget build(BuildContext context) { final localAvStatus = context.select( (WebRtcState state) => state.localAvStatus, ); return Scaffold( appBar: null, body: Stack( children: [ Column( children: [ SizedBox( height: MediaQuery.of(context).size.height / 2, child: const WebRtcView(), ), SizedBox( height: MediaQuery.of(context).size.height / 2, child: const RoomUserList(), ), ], ), Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.symmetric(vertical: 30), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ CallActionButton( tag: 'mic mute', icon: localAvStatus.mic ? Icons.mic : Icons.mic_off, color: Colors.black, backgroundColor: Colors.white, onPressed: () { context.read<WebRtcController>().toggleLocalAudio(); }, ), CallActionButton( tag: 'call end', icon: Icons.call_end, backgroundColor: Colors.red, onPressed: Navigator.of(context).pop, ), CallActionButton( tag: 'video mute', icon: localAvStatus.video ? Icons.videocam : Icons.videocam_off, color: Colors.black, backgroundColor: Colors.white, onPressed: () { context.read<WebRtcController>().toggleLocalVideo(); }, ), ], ), ), ) ], ), ); } }Demo
予め
iPad
の方を起動しておき、同じRoom
にJoin
しています。映像の遅延もなく、動作もサクサクで快適に動作しました。
Flutter
で手軽にWebRTC
を利用することができるのは嬉しいですね。終わりに
今回作成したサンプルアプリは GitHub に公開しているのでご自由にお使いください。
yukitaka13-1110 / flutter_webrtc_agora_example
コロナウイルスの影響もあってリモートワークやその他遠隔コミュニケーションでビデオ通話ができるツールがの需要が高まってきているのを感じているので、その裏側を作るのも面白そうだなと思いました。
現在、
QUANDO
ではiOS, Android, Flutter
エンジニア募集中です ← ここ重要