20200525のAndroidに関する記事は4件です。

RecyclerViewとLiveDataを使ってリスト表示

はじめに

今回は、前回の続きをやって聞きたいと思います。前回はRecyclerViewを使ってダミーのデータをリスト表示することができました。今回はEditTextで入力したデータをリスト表示できるようします。

目標

環境

  • Android Studio 3.6.3
  • Kotlin 1.3.72

作成手順

Gradle

build.gradle
dependencies {
    // 省略
    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.kt
class MainViewModel : ViewModel() {
    val todo = MutableLiveData<String>()

    fun addItem(item: String) {
        todo.value = item
    }
}

MainActivityの定義

ボタンが押された時にEditTextの値をMainViewModelの変数todoに代入します。その値の変化を監視してAdapterItemをセットする。

MainActivity.kt
class 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.kt
class 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を使ってデータベースに値を保存する方法について書きます!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android Studio activity_main.xmlでXMLではなくDesign Editorを使う方法

はじめに

activity_main.xmlを開くと、デフォルトでは視覚的に操作するDesign Editorが開くかと。ただし、たまにXMLの表示になっていることがある。
Android Studioを使い慣れていない方は、ここで戸惑うことがたまにある。。

そんな時のための記事

Design Editorに変更する方法

activity_main.xmlを開く
スクリーンショット 2020-05-25 14.55.28.jpg

スクリーンショット 2020-05-25 14.55.41.jpg

これでOK!

参考

https://developer.android.com/studio/write/layout-editor?hl=ja

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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のアイコンについて纏めてみた -Qiita

ldpi〜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"
}

参考にしたもの

cordova-plugin-firebase -GitHub

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FlutterでWebRTCをやってみる with AgoraSDK

はじめに

WebRTC を使ったアプリを開発する機会があり、何かお手軽な package ないかなーと探していたところ FlutterWebRTC を手軽に利用できる plugin を見つけたので試しにアプリを作成してみました。

WebRTC とは

WebRTC (Web Real-Time Communication) とは、ビデオや音声、データをブラウザ間でやり取り可能にするための規格で、Google によってオープンソース化されました。 ユーザーはその API を経由することでリアルタイム通信を実現できます。

参考: WebRTCの基本とP2P通信が成立するまでを学ぶ

最近ではコロナの影響もあり、ウェブ会議システムやチャットツールなどの利用者が急増しています。

  • Zoom
  • Hang out
  • Discord
  • Microsoft Teams

スクリーンショット 2020-05-25 04.37.36.png

コロナをきっかけに一般に広く知られるようになり、今まで利用するに至らなかった勢が利用していました(周りでも)。
今後これらのツールが一般的に利用されるようになるのではないでしょうか。

Agora.IO SDK

Agora.IO が開発している、ビデオ通話やライブ配信を構築できる SDK です。
基盤となるこの SDK を利用して様々な言語やプラットフォームで利用することができます。
日本では NTT CommunicationsSkyWay に相当するものです。

Flutter では agora_rtc_engine | Flutter package を利用します。

実装

実際にサンプルアプリを実装していきます.

Agora Project の作成

Agora を利用するには Agora.IOProject を作成し、 AppID を入手する必要があります.
無料枠が 10000 minutes 程あるので当分は無料で利用できますし、無料枠を超えても金額を請求されることはないので安心してください.

まずはこちらから登録して Project を作成します.
Agora | Sign Up

Project を作成後、Project Management をクリックして作成した ProjectAppID をメモしておきます.

スクリーンショット 2020-05-21 18.21.36.png

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.yaml
dependencies:
  agora_rtc_engine: ^1.0.12

Controller

今回は state_notifierfreezed パッケージを利用して実装しました。
agora_rtc_engine に関する処理は全部この中でやっています。

WebRtcController.dart
import '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.dart
AgoraRenderWidget(
    uid, /// Join に成功した場合に取得できる uid
    local: true, /// 自分であれば true, それ以外は false
    preview: true,
    mode: VideoRenderMode.Hidden /// object-fit か object-cover か
)

Page

webrtc_page.dart
import '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 の方を起動しておき、同じ RoomJoin しています。

映像の遅延もなく、動作もサクサクで快適に動作しました。
Flutter で手軽に WebRTC を利用することができるのは嬉しいですね。

終わりに

今回作成したサンプルアプリは GitHub に公開しているのでご自由にお使いください。

yukitaka13-1110 / flutter_webrtc_agora_example

コロナウイルスの影響もあってリモートワークやその他遠隔コミュニケーションでビデオ通話ができるツールがの需要が高まってきているのを感じているので、その裏側を作るのも面白そうだなと思いました。

 

現在、QUANDO では iOS, Android, Flutter エンジニア募集中です ← ここ重要

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む