20200927のAndroidに関する記事は11件です。

LINEの通知から色々な情報をスクレイピングする

はじめに

2011年6月23日にLINEサービスが開始され今年で9年目になりますが、国内の月間アクティブユーザー数が約8,700万人ということで、身近でもLINEを使っているという方はほとんどだと思います。
普段のやり取りは主にLINEで行うと思いますが、頻繁に使うLINEからデータが取れれば色々活用できると思い、どうにかやりとりの情報を取得できないかと考えました。

私はこの方法を使って、LINEの通知音・着信音を個別に鳴り分けるようにする「ピックアップ通知音」というアプリをGoogle Playに公開しています!
https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409628/e48b6416-da86-2db1-0c9a-0c1a5c4be66e.png
https://play.google.com/store/apps/details?id=rabbitp.sns.notifisort
https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409628/a4ce1b87-24e8-d6ae-c0ac-1e4ed07cf6f4.png
Google Play および Google Play ロゴは、Google LLC の商標です。

やり方をざっくり説明すると...

LINEでメッセージが届く

通知が表示される

通知内容を、Androidの「通知へのアクセス」機能を使って取得する

必要な情報のみスクレイピングする

こんな感じです。
詳しいやり方はこの後紹介します。
(前置きを飛ばしたい方はここをクリック)

スクレイピングの前に...「通知を取得する方法」

参考になるプロジェクトがあるのでそれを使うと便利です。
こちら→ https://github.com/oggata/NotificationListenerServiceDemo
ダウンロード後にAndroid Studioを開くと、次のようになります。
000261.png
2015年製のプロジェクトなので、Gradleバージョンが古いため警告が表示されます。
ファイルメニュー→プロジェクト構造...を開き、Android Gradle プラグインバージョンとGradle バージョンを任意のものに変更してOKをクリックしてください。
000262.png
(私はプラグイン4.0.0・Gradle6.1.1を使いました。)

OKをクリックした後、このような画面になったら、「repositories」の部分2箇所を次のように変更します。
000263.png

build.gradle
buildscript {
    repositories {
        google()
        jcenter()
    }

    ...省略

allprojects {
    repositories {
        google()
        jcenter()
        maven {
            url "https://jitpack.io"
        }
    }
}

修正が完了したら、左上の「再試行」をクリックして、同期が終わるまで待ちます。

すると再度エラーが出るので、build.gradleの「targetSdkVersion 21」を削除し、リンクになっている「Remove minSdkVersion and sync project」をクリックし、次の記述を削除します。
000264.png

AndroidManifest.xml
<uses-sdk
        android:minSdkVersion="18"
        android:targetSdkVersion="18" />

...他の部分は省略

削除したら、再度build.gradleを開いて右上の「今すぐ同期」をクリックします。
これでエラーがすべて消えるはずです。

このプロジェクトは、「通知が来たら、その生データをオーバーレイで画面の上から流れるように表示する」というものですが、確認に使うには少し不便なので改良します。

「通知へのアクセス」の設定画面を開くボタンを追加する

activity_main.xmlに適当にボタンを配置し、次のプログラムを追加します。

MainActivity.java
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //通知アクセス許可画面
                Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
                startActivity(intent);
            }
        });

オーバーレイをやめて、EditTextに表示するようにする

コピペできると良いと思ったので、今回はTextViewではなくEditTextを使います。
activity_main.xmlの全画面にEditTextを配置し、次のプログラムを追加します。

MainActivity.java
public class MainActivity extends Activity {
    EditText editText;
    String log = "";

...省略

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        editText = (EditText) findViewById(R.id.editText);

...省略

    class NotificationReceiver extends BroadcastReceiver{
        @Override
        public void onReceive(Context context, Intent intent) {
            String msg = intent.getStringExtra("notification_msg") + "\n";

            final DateFormat df = new SimpleDateFormat("MM/dd HH:mm:ss");
            final Date date = new Date(System.currentTimeMillis());
            log=df.format(date)+"\n"+msg+"----------------\n"+log;

            editText.setText(log);
        }
    }
}

新しい通知の方が上になるように表示されます。

リセットボタンを追加する

ログと表示をリセットするためのボタンを付けます。
activity_main.xmlに適当にボタンを配置し、次のプログラムを追加します。

MainActivity.java
        Button button2 = (Button) findViewById(R.id.button2);
        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //リセット
                log="";
                editText.setText("");
            }
        });

通知の詳細情報を取得する

「sbn.getNotification().extras」を使用すると、実際通知には表示されない情報も取得することができます。
送信者名やメッセージの全文などは、すべてこの中から取得することができます。

NLService.java
...省略

            i.putExtra("notification_msg", ""
                    + "ID :" + sbn.getId() + "\n"
                    + "Txt :" + sbn.getNotification().tickerText + "\n"
                    + "Pkg :" + sbn.getPackageName() + "\n"
                    + "Tim :" + sbn.getPostTime() + "\n"
                    + "extras :" + sbn.getNotification().extras+"\n"
                    + "\n");

...省略

アプリの実行画面

レイアウト等は変更している箇所があります。
https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409628/6d2f3141-695c-2efc-b2e8-9764cc7f5ad2.png

スクレイピング方法の目次

分かりやすいように、スクレイピングしたい情報ごとにまとめています。

【 判別 】
LINEの通知かどうか
メッセージか無料通話か

【 メッセージの場合 】
送信者名
対象グループ名
メッセージ内容
送信されたLINEスタンプのURL
送信者のプロフィール画像
チャットID
既読にした or 通知が削除された場合

【 無料通話の場合 】
発信開始
着信
不在着信
通話開始
通話終了

countStringInString関数について

プログラムの一部に、文字の出現回数を数えるcountStringInString関数が使用されています。

    //文字出現回数取得
    public static int countStringInString(String target, String searchWord) {
        //return (target.length() - target.replaceAll(searchWord, "").length()) / searchWord.length();

        Pattern p = Pattern.compile(searchWord);
        Matcher m = p.matcher(target);
        String result = m.replaceAll("");

        int out = target.length() - result.length();
        return out;
    }

判別

LINEの通知かどうか

記述場所:「NLService.java」の「onNotificationPosted」内

NLService.java
        if(sbn.getPackageName().equals("jp.naver.line.android")) {
            //ここにLINEの通知を受信した際にしたい処理を記述します

        }

メッセージか無料通話か

記述場所:「NLService.java」の「onNotificationPosted」内

NLService.java
        if (String.valueOf(sbn.getId()).indexOf("880002") != -1 || String.valueOf(sbn.getId()).equals("110000")) {
             //不在着信 or 無料通話系の通知ID の場合→無料通話

        } else {
             //違った場合→メッセージ

        }

記述場所:「NLService.java」の「onNotificationRemoved」内

NLService.java
        if (String.valueOf(sbn.getId()).equals("110000")) {
             //無料通話が拒否されたり終話ボタンが押されたりして終了した場合→無料通話

        }

メッセージの場合

送信者名

送信した相手の名前を取得できます。
個別とグループ、両方の場合で取得可能です。
記述場所:「NLService.java」の「onNotificationPosted」内

NLService.java
        //整形
        String ex=String.valueOf(sbn.getNotification().extras);
        ex=ex.substring(8);  //先頭から8文字削除「Bundle[{」
        String[] extars = ex.split(",");
        int dir = countStringInString(ex, ",");  //「,」の数

        //送信者名を抜き出す
        String Name="";
        for (int a = 0; a < dir; a++){
            if(extars[a].indexOf("android.title=")==0){
                //先頭に「android.title=」がきた→送信者名
                Name = extars[a].substring(14);  //先頭から文字削除「android.title=」
                break;
            }
        }
        if(Name.indexOf(" - ")!=-1){
            //Android5・6系ではNameが「[送信者名] - [グループ名]」になる
            Name=Name.split(" - ")[0];
        }
        //Name:送信者名

対象グループ名

メッセージが送信されたグループの名前を取得できます。
グループではなく個別だった場合はnullが返ります。
記述場所:「NLService.java」の「onNotificationPosted」内

NLService.java
        //整形
        String ex=String.valueOf(sbn.getNotification().extras);
        ex=ex.substring(8);  //先頭から8文字削除「Bundle[{」
        String[] extars = ex.split(",");
        int dir = countStringInString(ex, ",");  //「,」の数

        //グループ名を抜き出す
        String GroupName="";
        for (int a = 0; a < dir; a++){
            if(extars[a].indexOf("android.title=")==0){
                //先頭に「android.title=」がきた→送信者名
                GroupName = extars[a].substring(14);  //先頭から文字削除「android.title=」
                break;
            }
        }
        if(GroupName.indexOf(" - ")!=-1){
            //Android5・6系ではNameが「[送信者名] - [グループ名]」になる
            GroupName=Name.split(" - ")[1];
        }else {
            GroupName=sbn.getNotification().extras.getString(Notification.EXTRA_CONVERSATION_TITLE);
            if(GroupName!=null){
                Name=Name.substring(GroupName.length()+2);  //先頭に「: 」が付与されているから削除する
            }
        }
        //GroupName:グループ名

メッセージ内容

送信されたメッセージを取得できます。
通知では「...」と表示されて省略される場合でも、全文取得可能です。
記述場所:「NLService.java」の「onNotificationPosted」内

NLService.java
        //内容を抜き出す
        String Text = sbn.getNotification().extras.getString(Notification.EXTRA_TEXT);
        //Text:内容

送信されたLINEスタンプのURL

https://stickershop.line-scdn.net/products/」から始まり「.png」で終わるURLが取得できます。
このURLを元にHTTP GETすればスタンプの画像を表示することも可能です。
(HTTP GETして画像を表示する方法:https://akira-watson.com/android/httpurlconnection-get.html)
スタンプではなかった場合はnullが返ります。
ちなみに、画像サイズはスタンプによりバラバラですが大体120px以上はあります。
記述場所:「NLService.java」の「onNotificationPosted」内

NLService.java
        //スタンプの画像URLを抜き出す
        String StampURL="null";
        for (int a = 0; a < dir; a++){
            if(extars[a].indexOf(" line.sticker.url=")==0){
                //先頭に「line.sticker.url=」がきた→スタンプの画像URL
                StampURL = extars[a].substring(18);  //先頭から18文字削除「line.sticker.url=」
                break;
            }
        }
        //StampURL:スタンプの画像URL

送信者のプロフィール画像

Bitmap型で取得できるので、ImageViewに表示したりPNGとして保存したりできます。
画像サイズは108px × 108pxです。
記述場所:「NLService.java」の「onNotificationPosted」内

NLService.java
        Bitmap LargeIcon = drawableToBitmap(sbn.getNotification().getLargeIcon().loadDrawable(NLService.this));

...省略

    //Drawable型をBitmap型に変換
    public static Bitmap drawableToBitmap (Drawable drawable) {
        Bitmap bitmap = null;

        if (drawable instanceof BitmapDrawable) {
            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
            if(bitmapDrawable.getBitmap() != null) {
                return bitmapDrawable.getBitmap();
            }
        }

        if(drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
            bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel
        } else {
            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        }

        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);
        return bitmap;
    }

チャットID

友達やグループごとに決まっている識別子です。
これを使えば、同じ名前の友達やグループがあったとしても判別することができます。
恐らく、LINEの「トークショートカットを作成」機能はこのIDを使用したURLスキームだと思われますが、検索しても出てきませんでした...
※友達登録時に使うIDではありません。
記述場所:「NLService.java」の「onNotificationPosted」内

NLService.java
        //整形
        String ex=String.valueOf(sbn.getNotification().extras);
        ex=ex.substring(8);  //先頭から8文字削除「Bundle[{」
        String[] extars = ex.split(",");
        int dir = countStringInString(ex, ",");  //「,」の数

        //チャットIDを抜き出す
        String ChatID="";
        for (int a = 0; a < dir; a++){
            if(extars[a].indexOf("line.chat.id=")==0){
                //先頭に「line.chat.id=」がきた→チャットID
                ChatID = extars[a].substring(13);  //先頭から13文字削除「line.chat.id=」
                break;
            }
        }
        //ChatID:チャットID

既読にした or 通知が削除された場合

LINEのトーク画面を開いて既読にしたか、メッセージ通知を削除した場合に呼ばれます。
記述場所:「NLService.java」の「onNotificationRemoved」内

NLService.java
        if (String.valueOf(sbn.getId()).indexOf("880000") != -1) {  //15880000or16880000のどちらか?
            //既読にした or 通知が削除された場合

        }

無料通話の場合

次のような流れで処理が呼ばれます。

  • 電話をかけて通話せずに終了したとき:発信開始→通話終了
  • 電話をかけて通話して通話が終わったとき:発信開始→通話開始→通話終了
  • 着信があり相手が切ったとき:着信→通話終了→不在着信
  • 着信があり自分が切ったとき:着信→通話終了
  • 着信があったが反応せず「応答なし」になったとき:(着信があり相手が切ったときと同じ)
  • 着信があり通話して終話ボタンを押したとき:着信→通話開始→通話終了

発信開始

自分から電話をかけたときに呼ばれます。
記述場所:「NLService.java」の「onNotificationPosted」内

NLService.java
        if (String.valueOf(sbn.getId()).indexOf("110000") != -1 && Text.indexOf("発信中") != -1) {  //15880002or16880002のどちらか?
            //発信開始

        }

着信

電話がかかってきたときに呼ばれます。
記述場所:「NLService.java」の「onNotificationPosted」内

NLService.java
        if (String.valueOf(sbn.getId()).indexOf("110000") != -1 && Text.indexOf("着信中") != -1) {  //15880002or16880002のどちらか?
            //着信

        }

不在着信

着信があり相手が切った(拒否ボタンを押した or 応答がなかった)場合に呼ばれます。
記述場所:「NLService.java」の「onNotificationPosted」内

NLService.java
        if (String.valueOf(sbn.getId()).indexOf("880002") != -1 && Name.equals("LINE不在着信")) {  //15880002or16880002のどちらか?
            //不在着信

        }

通話開始

通話が開始されると呼ばれます。
記述場所:「NLService.java」の「onNotificationPosted」内

NLService.java
        if (String.valueOf(sbn.getId()).indexOf("110000") != -1 && Text.indexOf("通話中") != -1) {  //15880002or16880002のどちらか?
            //通話開始

        }

通話終了

何らかの方法で通話が終了したときに必ず呼ばれます。
記述場所:「NLService.java」の「onNotificationRemoved」内

NLService.java
        if (String.valueOf(sbn.getId()).equals("110000")) {
            //通話終了

        }

注意

これは実際にLINEの通知を受信してみて、どうやったらうまくスクレイピングできるかを検証して分かったやり方です。
LINEの仕様変更によっては、今後使えなくなる可能性が十分ありますのでご注意ください。
(実際に2018年に大幅な仕様変更があり、2020年にも一部変更されました。)

また、Android 7.0未満(?)では、試しているうちに「onNotificationPosted」が呼ばれなくなる不具合があるようです。
検証に使用する場合は、Android 7.0以上を使ってください。
また、minSdkVersionを24(Android 7.0)以上に設定しておくことをおすすめします。

参考サイト

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

[Android]Coil の Video Frames を使ってみる

はじめに

Coil の Video Frames を使うことで動画ファイルのあるフレーム画像を ImageView に表示できるらしい。今回はどのような感じで取得できるのか試してみたいと思う。

準備

Video Frames を使う場合には Core ライブラリに加えて Video Frames ライブラリを依存関係に追加する必要があります。次のように build.gradle に追記して Video Frames ライブラリを依存関係として追加します。

dependencies {
      
    implementation "io.coil-kt:coil:1.0.0-rc3"
    implementation("io.coil-kt:coil-video:1.0.0-rc3")
      
}

実装

raw フォルダに動画を格納する

動画ファイルが必要になるので次のように raw フォルダを作成し格納しておきます。
(対応している動画ファイルの拡張子→サポートされているメディア形式)

Image from Gyazo
Image from Gyazo

raw フォルダから動画ファイルを読み込む

まずは raw フォルダに格納したファイルの URL を生成し、load にてその url を読み込んでやります。あとは videoFrameMillis にて動画のどの秒数のフレームを表示するか指定してやれば動作します。

下記のサンプルだと fetcher で VideoFrameUriFetcher を指定してやっています。本来であれば Coil 側で動画ファイルであるか自動的に判定してくれるので指定する必要はないみたいなのですが URL を指定した場合だと上手く動作しないので追加しています。

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // RAWリソースのURLを生成するa
        val url = Uri.parse("android.resource://" + packageName + "/" + R.raw.movie)

        // fetcher に VideoFrameUriFetcher を指定してやると動画からフレームを切り出してくれる
        image_view.load(url) {
            videoFrameMillis(1000)
            fetcher(VideoFrameUriFetcher(applicationContext))
        }
    }
}

Image from Gyazo

また URL だけではなく File での読み込みもできるようになっているみたいです。URLの場合には VideoFrameUriFetcher を指定しましたが File の場合は VideoFrameFileFetcher を指定してやります。

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // ファイル
        val file = File("任意のファイルパス")

        // fetcher に VideoFrameFileFetcher を指定してやると動画からフレームを切り出してくれる
        image_view.load(file) {
            videoFrameMillis(1000)
            fetcher(VideoFrameFileFetcher(applicationContext))
        }
    }
}

おわりに

Coil の Video Frames では特定の動画ファイルから特定のフレームを画像として切り出すことができます。これを使えば動画ファイルの読み込み中に表示するサムネイルの取得などができそうです。使う機会が多いとは言えない機能ですが頭の片隅に覚えていれておけばどこかで使えそうだと思いました。

参考文献

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

【Android】Bindingした値をトリガにいい感じにアニメーションする(今回の対象:背景色)

やりたいこと

「ViewModelクラスに持ってる値に応じて、Viewの背景色を変えたい。変わる時にはアニメーションしてほしい」

やったリポジトリ

https://github.com/nredjap/android-bindAnimation

実装内容の話

プラスマイナスののカウンターがあって、0だと背景グレーで、それ以外だと別の色。
この色変更をアニメーションさせたい。そんな実装をしました。

データ側

ViewModel側に色情報も持って、LiveDataに突っ込みました。

MainViewModel.kt
// ~上略~
    private val _qty = MutableLiveData(0)
    val qty: LiveData<Int> = _qty

    val cardColor: LiveData<Int> = Transformations.map(_qty) {
        when (it) {
            0 -> Color.LTGRAY
            else -> Color.CYAN
        }
    }
// ~下略~

レイアウト側

時間をかけて色を変更するBindingAdapterを定義します。たぶん今回のキモ。

BindingUtils.kt
@BindingAdapter("backgroundColor_gradually")
fun bindBackgroundColorGradually(view: View, oldColor: Int, newColor: Int) {
    if (oldColor == newColor) return
    val background = view.background
    if (background is ColorDrawable) {
        ValueAnimator.ofArgb(background.color, newColor).apply { // ここでoldColorを使うと色変化中に戻すような時に中間色が使われない
            duration = 500
            addUpdateListener { view.setBackgroundColor(it.animatedValue as Int) }
            start()
        }
    } else {
        view.setBackgroundColor(newColor)
    }
}

背景色を変えたいViewにbackgroundColor_graduallyを定義します。

main_fragment.xml
<!-- ~上略~ -->
        <com.google.android.material.card.MaterialCardView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:backgroundColor_gradually="@{vm.cardColor}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">
<!-- ~下略~ -->

おわりに

本質的には「ValueAnimatorつかってみた」だけの内容になってしまった感がありますが、
いい具合に複雑にならずにViewModelからXMLへのBindingで書けたと思います。満足。

BindingAdapter関数の内容のカスタマイズとLiveDataに持つ値の対応で、
Viewの回転とか座標とかサイズとかもアニメーションできると思います。

参考

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

RecyclerViewのレシピ

RecyclerViewをいざ使うときに忘れがちなのでRecyclerViewの導入手順から順を追ってを纏めようと思います。

下準備

RecyclerViewを使う前に下準備をします。

build.gradle
    dependencies {
        // 追加
        classpath 'com.android.support:recyclerview-v7:28.0.0'
    }
app/build.gradle
    dependencies {
        // 追加
        classpath 'com.android.support:recyclerview-v7:28.0.0'
    }

表示するアイテムのレイアウトを作成

今回はListを表示していくので各要素の1つ1つがこのレイアウトになります。

list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="horizontal"
    android:layout_margin="5dp"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >

    <ImageView
        android:id="@+id/image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:contentDescription="@null"
        app:srcCompat="@mipmap/ic_launcher"
        />

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:textSize="20sp"
        tools:text="TextArea"/>

</LinearLayout>

ViewHolderを作る

RecyclerViewHolder.kt
package com.example.recyclerviewproject

import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.list_item.view.*

class RecyclerViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    val itemImage = view.image
    val itemText = view.text
}

Adapterを作成する

RecyclerViewAdapter.kt
package com.example.recyclerviewproject

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView

class RecyclerViewAdapter(private val context: Context, private val itemList: List<String>) :
    RecyclerView.Adapter<RecyclerViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {
        val layoutInflater = LayoutInflater.from(context)
        val item = layoutInflater.inflate(R.layout.list_item, parent, false)
        return RecyclerViewHolder(item)
    }

    override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
        holder.let {
            it.itemImage.setImageResource(R.mipmap.ic_launcher)
            it.itemText.text = itemList[position]
        }
    }

    override fun getItemCount(): Int = itemList.size
}

下準備は以上で完了です。
次は下準備した材料を呼び出していきます。

盛り付け

MainActivity.kt
package com.example.recyclerviewproject

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val listItem = resources.getStringArray(R.array.item_list).toMutableList()

        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        recyclerView.adapter = RecyclerViewAdapter(this, listItem)
        recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
    }
}

以上で盛り付けまで完成です。
では、表示してみます。

表示

recyclerview.png

CardViewで少し整えます

以下を追加します。

build.gradle
classpath 'com.android.support:cardview-v7:28.0.0'
app/build.gradle
implementation 'androidx.cardview:cardview:1.0.0'

レイアウトを少し修正します。

list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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="wrap_content"
    android:layout_margin="8dp"
    android:foreground="?android:attr/selectableItemBackground">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:contentDescription="@null"
            app:srcCompat="@mipmap/ic_launcher" />

        <TextView
            android:id="@+id/text"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:textSize="24sp"
            tools:text="TextArea" />

    </LinearLayout>
</androidx.cardview.widget.CardView>

整ったので表示してみます。

表示

recyclerview.png

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

HMSのプッシュ通知の実装説明(サーバー側)

前書き

前章(HMSのプッシュ通知の実装説明(Android側))ではHMSのプッシュ通知のAndroid側の実装について説明しました。本章ではサーバー側の実装について説明します。

サーバー側の実装

サーバーでやることはたった2つだけです。

  1. プッシュサーバーからアクセストークンを取得すること
  2. プッシュサーバーにプッシュ通知を送ること

もっと具体的にわかりやすく説明すると、以下の2つのAPIを順番に叩くだけで、実装が完了します。

順番 API URL 説明
1 アクセストークン取得 https://oauth-login.cloud.huawei.com/oauth2/v2/token
2 プッシュ通知送信 https://push-api.cloud.huawei.com/v1/[appId]/messages:send [appId]はAppGallery ConnectのMy AppsのApp informationのApp IDです

プッシュサーバーからアクセストークンを取得

こちらはPOSTMANのスクリーンショットです。API実装経験がある方なら、これだけですぐ理解できると思います。

access_token.png

リクエストボディにAppGallery Connectに書かれているApp IDとApp secretを載せ、APIを叩けば、アクセストークンが返ってきます。

リクエスト

API:https://oauth-login.cloud.huawei.com/oauth2/v2/token
メソッド:POST
Content-Type:application/x-www-form-urlencoded

リクエストボディ:

キー
grant_type “client_credentials”で固定
client_id AppGallery ConnectのMy AppsのApp informationのApp ID
client_secret AppGallery ConnectのMy AppsのApp informationのApp secret

レスポンス

Content-Type:application/json;charset=UTF-8

キー
access_token アクセストークン
expires_in アクセストークンの有効期限(秒)
token_type Bearerで固定

プッシュサーバーにプッシュ通知を送信

push_token_1.png
push_token_2.png

リクエストヘッダーに先ほど取得したアクセストークンを載せ、リクエストボディにプッシュ通知内容を載せ、APIを叩けばよいです。

リクエスト

API:https://push-api.cloud.huawei.com/v1/[appid]/messages:send
[appid]はAppGallery ConnectのMy AppsのApp informationのApp IDです

メソッド:POST
Content-Type:application/json

リクエストヘッダー

キー
Authorization アクセストークン

リクエストボディ:
JSONになります。パラメータが多すぎるので、オフィシャルサイトをご参照ください。
https://developer.huawei.com/consumer/jp/doc/HMSCore-References-V5/https-send-api-0000001050986197-V5

AppGallery Connect

App IDとApp secretはAppGallery Connectに載っています。

AppGallery Connect - My apps.png

まとめ

サーバー側の実装は、プッシュ通知の送信部分に関しては、この2つのAPIの使い方さえ押さえれば、難しいことではないでしょう。

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

HMSのプッシュ通知の実装説明(Android側)

HMSのプッシュ通知の概要

プッシュ通知を一言で説明すると、サーバー側(たとえば企業側)がクライアント側(たとえばユーザー側)に通知を送るためのサービスです。
(実際、相手のプッシュトークンさえ分かれば、クライアント側からHMSのプッシュサーバー経由で相手のクライアントへプッシュ通知を送ることもできるのですが、話がややこしくなるので、ここではあくまでサーバー側からクライアント側へ送るプッシュ通知にのみ焦点を当てます。)

トークン

プッシュ通知を利用するのに、まず理解しなければならないのはトークンという概念です。プッシュ通知には2種類のトークンがあります。

  • プッシュトークン(Push Token)
  • アクセストークン(Access Token)

プッシュトークン

クライアントを特定するためのユニークな番号です。クライアント側がサーバー側にプッシュトークン発行を要求し、サーバー側がプッシュトークンをクライアント側に返します。また、サーバー側がクライアント側にプッシュ通知を送るときに、プッシュトークンを使ってクライアントを特定し、プッシュ通知を送ります。プッシュトークンはクライアント側もサーバー側も使います。

アクセストークン

サーバー側がクライアント側にプッシュ通知を送るために必要な鍵です。この鍵を持って、特定なプッシュトークン(クライアント)向けにプッシュ通知を送ります。アクセストークンはサーバー側のみ使います。

プッシュ通知の動作の流れ

オフィシャルサイトの説明によると、プッシュ通知の流れは次の図に示すようになります。
HMSプッシュ通知説明

STEP 1:
クライアントがHmsInstanceIdのgetToken()を呼び出し、プッシュサーバーにプッシュトークンを要求します。

STEP 2:
プッシュサーバーがクライアントにプッシュトークンを返します。

STEP 3:
クライアントが自分のプッシュトークンを自分のサービスのサーバー(アプリサーバー)に送ります。

STEP 4:
アプリサーバーがプッシュサーバーにプッシュ通知を送ります。
(ここではさらに2ステップに分けることができますが、次章(HMSのプッシュ通知の実装説明(サーバー側))で詳細を説明します)

STEP 5:
プッシュサーバーがアプリサーバーから受け取ったプッシュ通知をクライアントに転送します。

STEP 6:
クライアントがプッシュ通知を受け取った結果をプッシュサーバーに知らせます。

STEP 7:
プッシュサーバーをその結果をアプリサーバーに転送します。

実装について

実装が必要なステップは4つしかありません。Android側に3ステップとサーバー側に1ステップです。

ステップ 実装場所 関わるトークン
STEP 1 Android プッシュトークン
STEP 2 Android プッシュトークン
STEP 3 Android プッシュトークン
STEP 4 サーバー アクセストークン、プッシュトークン
STEP 5 プッシュトークン
STEP 6
STEP 7

Android側の実装

AppGallery Connectでプッシュサービスを開通

  1. AppGallery Connectに入って、[My projects]を選びます。
  2. リストから対象アプリに切り替えます。
  3. [Growing] -> [Push Kit]に入って、Push Kitを有効にします。 HMSプッシュ通知開通

HMS SDKの導入

  1. agconnect-services.jsonをプロジェクト内に配置します。
  2. プロジェクトのbuild.gradleにSDKを追加します。
  3. モジュールのbuild.gradleにSDKを追加します。
  4. AndroidManifest.xmlにサービスを追加します。
AndroidManifest.xml
<service
    android:name=".DemoHmsMessageService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.huawei.push.action.MESSAGING_EVENT"/>
    </intent-filter>
</service>

詳細はこちらのオフィシャルサイトをご参照ください。
https://developer.huawei.com/consumer/jp/doc/development/HMSCore-Guides/android-integrating-sdk-0000001050040084

ソースコード

ステップ1~ステップ3のソースコードは次のようになります。

GetTokenClass.kt
Single.create<String>{ emitter ->
    // ステップ1+ステップ2
    val token = HmsInstanceId.getInstance(context).getToken(appId, "HCM")

    // 空のトークンが返ってくる可能性があるので、しっかりチェックしましょう
    if (token.isNotEmpty()) {
        emitter.onSuccess(token)
    } else {
        emitter.onError(Exception("Token is empty"))
    }
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ token ->
    // ステップ3:トークンをサーバーに送ります。
    // サーバーにトークンを送らないと、サーバーがこちらにプッシュ通知が送れません。
},{
    // エラー処理
})

オフィシャルサイトによると、getTokenを呼び出しても、空のトークンが返ってくる可能性があるので、HmsMessageServiceを継承し、onNewTokenをオーバーライドしなければなりません。

YourHmsMessageService.kt
class MainService: HmsMessageService() {
    // ステップ2
    override fun onNewToken(token: String?) {
        // ステップ3:トークンをサーバーに送ります。
        // サーバーにトークンを送らないと、サーバーがこちらにプッシュ通知が送れません。
    }
}

サーバー側の実装

こちらのページをご覧ください。
https://qiita.com/Rei_2020/items/a35ce101599e651ba3ac

まとめ

プッシュ通知の実装が難しそうに見えますが、実際にやってみたら案外簡単です。トークンの概念をしっかり理解すれば、全体像がすぐ把握できると思います。

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

Androidのレイアウトを1から整理する―ViewGroup編

Androidアプリ開発の入門書は、ActivityやらFragment、APIの使い方で終わるケースがほとんどな印象。
layout系知識の基本がまとまった新しめの書籍が欲しいですが、amazon検索は新しくても2014年のものとか出してくるので購入はためらいます。ってことで、勉強がてらまとめてみる。

対象

なんでもいいので入門書1冊を一通り終えたくらいのレベル感の方向けの記事になると思います。

当記事の目標

02Goal.png

こんなのを作ってレイアウトのViewGroupとはなんぞやって部分を理解する。

素材先: 
ひこちゃんず!

Androidレイアウトの全体像

01Layout構造.png

全体像は上記図が一番つかみやすい.

・View

ウィジェットとよばれる画面部品。ボタン(Button)やテキスト(TextView)などいろいろ。
ウィジットってよんだり、Viewってよんだり、画面部品ってよんだりしますが言っていることはだいたい同じです。

・ViewGroup

Viewの親要素になる。Viewを配置する際に特定のViewGroupの中に配置したほうがレイアウト調整が楽になる。CSSで言うと便利な親要素。
LinearLayoutやConstraintLayoutなど。入門書ではLinearlayoutを使うことが多かった印象ですが、古い様子。。
普段はレイアウトって呼ぶことのほうが多いです。レイアウトだと抽象度が高い表現なんで最初は混乱しますね。

以下羅列

  • View

    • TextView
    • EditText
    • Button
    • ImageView
    • ListView。。。etc
  • ViewGroup

    • FrameLayout
    • LinearLayout
    • TableLayout
    • GridLayout
    • RelativeLayout
    • ConstraintLayout。。。etc

ViewGroupに関してくやしく

線形レイアウト-- LinearLayout

ViewGroupの一種です。子要素を横方向または縦方向の1行にまとめるレイアウト。

相対レイアウト -- RelativeLayout

子要素の位置を親要素に対して相対的に指定ができる。LinearLayoutよりも柔軟な指定方法ができたリする。

制約レイアウト --ConstraintLayout

Viewの各辺1つ1つに、ほかのViewとの関係性を付けていくことで全体を調整する。最近の入門書ではこれを解説していることも多いかと。

キモリを配置するにはどうするか

さて本題。キモリを配置するにはどうするか。

02Goal.png

境界線を考える。

パット見、上側と下側で部品がわかれそうなのでこんな感じで分けてみる。

03kimori_separate.png

多分、図にするとこんな感じ。
ViewGroupGraf.png

LinearLayoutでキモリを配置する。

※目的はViewGroupを理解するです。そのため、View部品のコードに関しては省略します。そのうち詳しく書きます。

activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="7"
        android:orientation="vertical"
        android:gravity="center"
        android:background="#beedbe">

        <ImageView  ~~キモリの画像~~/>

    </LinearLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="3"
        android:background="#404251">

        <View ~~下画面の赤枠用部品~~ />

        <TextView ~~おや キモリのようすが の部品~~ />

    </FrameLayout>
</LinearLayout>

(TextViewが長いので若干の修正は加えたいと思いつつ)。。。。

図の通りの記載方法で再現ができました。

まず、一番大枠のLinearLayoutのViewGroupで

android:orientation="vertical"

を指定して、子要素のViewGroup二つを垂直に配置します。子要素のViewGroupたちには

android:layout_weight

をつかって、7:3の割合で画面を分割してます。
せっかくなので、一番下のViewGroupもLinearLayoutにしたかったのですが赤枠を表現することが大変そうだったのでこちらを参考にFrameLayoutで行いました。

RelativeLayoutでキモリを配置する。

方法はほぼ同じ

activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <RelativeLayout
        android:id="@+id/ViewGroup1"
        android:layout_width="match_parent"
        android:layout_height="280dp"
        android:background="#e1f6cf" >
        <ImageView
            android:layout_centerInParent="true"
             ~~キモリの画像~~/>
    </RelativeLayout>
    <FrameLayout
        android:layout_below="@+id/ViewGroup2"
        android:id="@+id/ViewGroup2"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#404251">
        <View ~~下画面の赤枠用部品~~ />

        <TextView ~~おや キモリのようすが の部品~~ />

    </FrameLayout>

</RelativeLayout>

ViewGroupたちへの属性から見ていきま。
RelativeLayoutは親要素に対して子要素をどう配置するかを考える。

ViewGroupGraf.png

  • 一番上のViewGroupがRelativeLayout
  • 赤枠ViewGroupもRelativeLayout
  • 青枠ViewGroupがFrameLayout

今回使っているRelativeLayout特有の属性は下記です。

android:layout_below="@+id/ViewGroup1"

赤枠レイアウトに対して、青枠レイアウトを下に配置するための属性です。コード内ではIDで配置を指定します。

android:layout_centerInParent="true"

キモリの画像に属性を指定しています。これにより、赤枠ViewGroupに対してキモリの画像を中央に配置できます。

ポイントは親にRelative、子に属性を指定すること。
配置方法は属性によって決まるので、詳しい属性は下記サイトの真ん中らへんにまとまってます。
てっくあかでみー

ConstraintLayoutでキモリを配置する

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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/main_battle_area"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:background="#e1f6cf"
        app:layout_constraintBottom_toTopOf="@+id/guideline"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <ImageView
            android:layout_width="160dp"
            android:layout_height="160dp"
            android:src="@drawable/kimori_01"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <androidx.constraintlayout.widget.Guideline
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/guideline"
        app:layout_constraintGuide_percent="0.7"
        android:orientation="horizontal"/>

    <FrameLayout
        android:id="@+id/frameLayout2"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:background="#404251"
        android:padding="3dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/guideline" >

        <View ~~下画面の赤枠用部品~~ />

        <TextView ~~おや キモリのようすが の部品~~ />

    </FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

長いですね。ConstraintLayoutはAndroidStudioのデザイン機能を使ってグラフィカルに操作することが多いと思うので詳細は省こうかと。

app:layout_constraintBottom_toBottomOf="parent"

layout_constraintHOGEHOGEの部分でどこの部品と関係付けるか指定しています。

このレイアウトのメリットは以下なよう

  • レスポンシブデザイン(画面サイズに応じたデザイン)に対応しやすい
  • ネストが浅くなるので処理が速くなる

ConstraintLayoutは使用経験がないので、あくまでらしいです。
ネストが深くなる=悪というのは昔からあるようなので、今後増えていくと予想されてますね。

おわり

最近ポケモンエメラルドのバトルタワーをやってます。この時代のゲーム画面なら表現できるのではと試しにまとめてみました。
書いている際に見つけた下記サイトがとても役に立った。FrameLayoutは結構パクりました。

Androidのレイアウトで遊んでみる1

(残念ながら途中で疾走してそうなのですが。。。)

次回はView部品を詳しくかけたらなと考えております。

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

個人開発アプリ(Android)がダウンロードされない

私ほどAndroidアプリがダウンロードされない開発者を他に知らないため、
同じ境遇の方もいるかと思い、現況と考察を書き記す。
残念ながら解決策は現段階では分からない。

先に結論

人望のない人間がただ単にSNSを使ってリリース報告したからといって、
アプリケーションが使われる(ダウンロードされる)ことはない。

状況

・Flutterでアプリを開発して、Google Playに公開
(Appleにもdeveloper申請したが、承認に掛かる時間が長くてAndroid版のみ先行リリース)
・人生初めてのGoogle Playへの公開
・筆者には友達がいない少ない

リリースしたアプリ

・する/しない
・今日何をするか
・おみくじ
・筋トレメニュー
・ランダム数字
のメニューを選んでもらい、
乱数で決断を代行するシンプルなアプリケーション

広告やシェアボタンも入っているので、
シンプルアプリケーションの模範になれるような出来ではあると思う。
(自分で言うのもなんですが)

アプリケーションの配信先はこちら↓
決断代行(Google Play)

ダウンロードしてもらうために行ったこと及び結果

SNSへ投稿
・twitterにリリースしたことを投稿する → 効果なし
・Facebookにリリースしたことを投稿する → 効果なし
・LINEのタイムラインにリリースしたことを投稿する → 効果なし
・LINEの親しいグループにリリースしたことを発表する → 1ダウンロード獲得!
以上。

Google Play Consoleでダウンロード数を把握できるが、
私を含めてダウンロード数は2であった。

考察

日本はiOSユーザに満たされてしまっていると考えられる
ここまでダウンロードされなかった者を他に知らないため、何か根本的に間違ったアプローチをしてしまったのだと思う。
今回の場合、1ダウンロードを獲得できただけでも上出来だったのかもしれない。

あまり意味ないかもしれないが、個別に振り返る。

twitter

フォロワー数が130くらいのアカウント。
TLに埋もれて見られる機会がなかった可能性もあるが、
「1いいね」を獲得出来た。
初めてのFlutter、初めてのGoogle Playへの公開だったためもっといいねされたかった。
どうでもいい投稿がいいねされまくってるのに、
エンジニアとしての挑戦や資格取得を褒めてくれないのがtwitter
そんな中、1いいねしてくれた方は神

Facebook

見てる人がそもそも少ないと思われる。
友達75人くらいのアカウントで「0いいね」を獲得した。

LINEのタイムライン

こちらも見ている人がそもそも少ないと思われる。
8割以上はツムツムで繋がっただけの友達600人くらいのアカウントで「0いいね」を獲得した。

LINEの親しいグループ

高校同期の9人グループ(男only)で報告したところ、
1人にダウンロードして貰えた。
あれ、他はiOSユーザってことだよね?、、

総合的な要因

・筆者に友達がい・・
・そのアプリケーションが相手に求められていない
・アプリケーションのメリット(面白さ)を伝えられる投稿内容になっていない
・投稿を閲覧されている数が少ない
・投稿にいいねを付けてはいけない雰囲気がある

今後

動画やキャッチーなフレーズを使ったり、あるいは別のメディアを使ったりして、
どうすればダウンロードして貰えるかを検証していきたい。

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

リリースした個人開発アプリ(Android)がダウンロードされない方法

私ほどAndroidアプリがダウンロードされない開発者は存在しないことを願うが、
同じ境遇の方に向けて、状況と考察を述べる。
ダウンロードされなかった方法が分かっているだけであり、ダウンロードされる方法については一切分からない。

先に結論

人望のない開発者が単にSNSを使ってリリース報告をしただけでは、
アプリケーションがダウンロードされるということはない。

状況

・Flutterでアプリを開発して、Google Playに公開
(Appleにもdeveloper申請したが、承認に掛かる時間が長くてAndroid版のみ先行リリース)
・人生初めてのGoogle Playへの公開
・筆者には友達がいない少ない

リリースしたアプリ

・する/しない
・今日何をするか
・おみくじ
・筋トレメニュー
・ランダム数字
のメニューを選んでもらい、
乱数で決断を代行するシンプルなアプリケーション

アプリケーションの配信先
決断代行(Google Play)

omikuji.png
omikuji.png

ダウンロードしてもらうために行ったこと及び結果

SNSへ投稿
・twitterにリリースしたことを投稿する → 効果なし
・Facebookにリリースしたことを投稿する → 効果なし
・LINEのタイムラインにリリースしたことを投稿する → 効果なし
・LINEの親しいグループにリリースしたことを発表する → 1ダウンロード獲得!
以上。

Google Play Consoleでダウンロード数を把握できるが、
私を含めてダウンロード数は2であった。

考察

日本はiOSユーザに満たされてしまっていると考えられる
ここまでダウンロードされなかった者を他に知らないため、何か根本的に間違ったアプローチをしてしまったのだと思う。
今回の場合、1ダウンロードを獲得できただけでも上出来だったのかもしれない。

あまり意味ないかもしれないが、個別に振り返る。

twitter

フォロワー数が130くらいのアカウント。
TLに埋もれて見られる機会がなかった可能性もあるが、
「1いいね」を獲得出来た。
初めてのFlutter、初めてのGoogle Playへの公開だったためもっといいねされたかった。
どうでもいい投稿がいいねされまくってるのに、
エンジニアとしての挑戦や資格取得を褒めてくれないのがtwitter
そんな中、1いいねしてくれた方は神

Facebook

見てる人がそもそも少ないと思われる。
友達75人くらいのアカウントで「0いいね」を獲得した。

LINEのタイムライン

こちらも見ている人がそもそも少ないと思われる。
8割以上はツムツムで繋がっただけの友達600人くらいのアカウントで「0いいね」を獲得した。

LINEの親しいグループ

高校同期の9人グループ(男only)で報告したところ、
1人にダウンロードして貰えた。
あれ、他はiOSユーザってことだよね?、、

総合的な要因

・筆者に友達がいな・・
・良いアプリを作れば使われるという幻想があった
・アプリケーションのメリット(面白さ)を伝えられる投稿内容になっていない
 →投稿に分かりやすい動画を使っていない
  →写真を添付して投稿したが内容がぱっと見分かりづらい
・そのアプリケーションが相手に求められていない
・投稿を閲覧されている数が少ない
・投稿にいいねを付けてはいけないような雰囲気が滲み出ている
・自分に合った適切なプロモーション手段を知らない(←結局、これが全て)

何かアドバイスや考察し切れていないダメな部分が他にございましたら、
ご指摘やコメントいただけましたら幸いです。

今後

動画やキャッチーなフレーズを使ったり、あるいは別のメディアを使ったりして、
どうすればダウンロードして貰えるかを検証していきたい。

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

『Android Jetpackプログラミング』Chapter2 最新バージョン対応(1.0.0-alpha03)

はじめに

本稿は秀和システムより発売されたAndroid Jetpackプログラミングに関する内容です。

『Android Jetpackプログラミング』のChapter2ではJetpackのComposeに関して扱っています。

書籍ではComposeのバージョンとして"0.1.0-dev13"が使用されていますが、アルファ版のためか頻繁に更新されているため、現在の最新環境では掲載されているコードのビルドが通らないことがあります。

今回は掲載コードのうち現在の環境でも動作するように必要な変更箇所についてまとめました。

※目的としてはビルドを通し、元コードと同じように動作をさせることとなります。変更後コードの詳細な解説等はありません。

実行環境

今回の動作検証に用いた環境です。

Android Studio 4.2 Canary 11

compose_version = '1.0.0-alpha03'

stateのDeprecated

最新環境ではstate { ... }がDeprecatedとなっており、remember { mutableStateOf(...) }へ置き換える必要があります。

そのため全サンプルコードでstate{ ... }が使用されている箇所を変更する必要があります。※現時点ではDeprecated指定となっていますが、定義はそのままのため変更なしでも動作します。

例えばリスト2-6では以下のようになります。

var state = remember { mutableStateOf(TextFieldValue())}

stateでの{}内部の宣言を、mutableStateOf()内部に移動するだけで置き換えが可能です。

https://android-review.googlesource.com/c/platform/frameworks/support/+/1375120

TextFieldによるテキスト入力

書籍では"androidx.ui.fundation.TextField"を使用していますが、TextFieldはBaseTextFieldに名前変更されています。

ただし、そのままTextFieldをBaseTextFieldに置き換えても、Android Studio上では以下の警告文が発生します。

This foundation API is experimental and is likely to change or be removed in the future.

どうやらBaseTextFiledを使用するためには「@ExperimentalFoundationApi」というアノテーションが必要とあるようです。このアノテーションをfuc Greeting()に付与すると、これを呼び出す側にも同じアノテーションを付与する必要があります。

"androidx.ui.fundation.BaseTextField"とは別に"androidx.compose.material.TextField"も存在します。こちらを使用したコードは以下です。

コード2-6
TextField(
        value = state.value,
        onValueChange = { state.value = it },
        label = { Text("TextField") }
)

https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#textfield

Modifier.NoneのDeprecated

Modifierは何度が登場しますが、Modifier.NoneはDeprecatedとなっています。Modifier.Noneは例えばリスト2-7のButtonクラス内部で使用されています。Android studioでの警告どおり('None: Modifier' is deprecated. use the Modifier companion object instead) に置き換えれば問題ありません。

コード2-7
Button(
        modifier = Modifier,
        onClick = {
            val n = state.value.text.toInt()
            var total = 0
            for (n in 1..n) {
                total += n
            }
            message.value = "total: $total"
        }) {
    Text(
            text = "Click",
            style = TextStyle(
                    color = Color.White,
                    fontSize = 20.sp
            )
    )
}

RadioButtonを用いた複数のラジオボタン表示

複数のラジオボタンを表示するRadioGroupが廃止され、使用不可となっています。代替手段としてRadioButtonとRowを用いて実現するようになっています。

書籍と同じ機能を実現するためのコードは以下です。これはAPI referenceのサンプルコードを参考にしています。

コード2-10
Column(){
        val radio = listOf("One", "Two", "Three")
        val (radioSelected, onRadioSelected) = remember { mutableStateOf(radio[0]) }
        Text(
            text = "selected: "  + radioSelected,
            style = TextStyle(
                color = Color.Red,
                fontSize = 28.sp
            )
        )
        radio.forEach { text ->
            Row(Modifier.fillMaxWidth()
                    .selectable(selected = (text == radioSelected),
                                onClick = { onRadioSelected(text)}
                    )
                    .padding(horizontal = 10.dp)
            ){
                RadioButton(
                        selected = (text == radioSelected),
                        onClick = { onRadioSelected(text) }
                )
                Text(
                        text = text,
                )
            }
        }
}

AlertDialogの非表示関数

ダイアログ外の領域やバックボタンを押した際に呼び出されるコールバック関数がonCloseRequest()からonDismissRequest()に変更になりました。それ以外の変更はなさそうなので、onCloseRequest()をonDismissRequest()に置き換えれば問題ありません。

コード2-12
AlertDialog(
        onDismissRequest = {
            dlog.value = false
        },
        title = {
            Text(text = "Alert")
        },
        text = {
            Text("This is sample message!")

        },
        confirmButton = {
            Button(
                    onClick = {
                        dialogMessage.value = "You select \"OK\"!!"
                        dlog.value = false
                    }
            ) {
                Text("OK")
            }
        },
        dismissButton = {
            Button(
                    onClick = {
                        dialogMessage.value = "You canceled..."
                        dlog.value = false
                    }
            ) {
                Text("Cancel")
            }
        },
)

drawRoundRectでの丸み指定

角が丸い四角形を描画するdrawRoundRectですが、角の丸みを指定する引数が変更となっています。

コード2-14
fun drawRoundRect(
    color: Color, 
    topLeft: Offset = Offset.Zero, 
    size: Size = this.size.offsetSize(topLeft), 
    radius: Radius = Radius.Zero, 
    ....
): Unit    

丸みは第4引数でandroidx.compose.ui.geomerty.Radiousクラスを用いて指定します。

drawRoundRect(Color.Blue, Offset(100f, 100f), Size(300f, 300f), Radius(25f, 25f))

丸みの横、縦幅を個別にFloatで指定するのではなく、Radiusインスタンスとして渡すことで表示できます。

drawLineでのStroke指定

drawLine()でのStrokeの指定方法が変更となっています。書籍ではStrokeクラスでwidth, miter, StrokeCapを指定し、drawLineの第4引数で渡すことでストロークを設定していました。

最新のdrawLine()ではStrokeクラスでの指定から、直接widthとStrokeCapを引数で指定するように変更されています。

コード2-16
drawLine(
        Color.Red,
        Offset(s1 * r + center.x, c1 * r + center.y),
        Offset(s2 * r + center.x, c2 * r + center.y),
        20f,
        StrokeCap.Round
)

Offsetクラスのメンバ変数

表示位置を指定する際に使用するOffsetクラスに関してはプロパティ名に変更があります。

x座標のoffsetは "dx -> x"、y座標のoffsetは "dy -> y"となっています。

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

Android2015年知識の私が2020年のAndroid開発事情に追いつく

https://qiita.com/hikaruna/items/d0cd8f4d82aa03302689
の記事を書いた頃である5年前から今までAndroidから離れていましたが、また触る機会を得ました。Androidのことだからどうせ原型を留めないほどに変わっているだろうなと覚悟しながら望むつもりです。

kotlin

kotlinを使えという世界になったらしい。
javaを選択することはできますし、ファイル単位で共存ができますが、今更新たにjavaを書くべきではない、kotlinから逃げてはいけないと、私は理解しました。

大体javaをswiftっぽくかけるだけだろうと舐めて掛かったのですが、以下が読めなくて無事死亡しました。

  • primary constructor
  • combine object
  • by

このあたりは、周辺言語の経験で雰囲気を掴む感覚が働かなかったのでおとなしく公式ドキュメント読みました。

コルーチンまわりは難しすぎてまだよくわかっていません。パット見iOSと似てきた気がします。

jetpack

よくわからないですがサポートライブラリのセットらしいです。

androidx

従来のサポートライブラリを置き換える、すごいサポートライブラリっぽい。androidx ∋ jetpack
従来とは違ってSDKのバージョンとは完全に無関係に、ライブラリのバージョンが管理されている。
なので、とりあえずあらゆる実装はなるべくandroidxパッケージのものを使うべきだと、私は解釈した。何も考えたくない。activityしかりfragmentしかり。何もかもすべて。

KTX

androidxライブラリが提供している、androidのAPIをkotlinで使いやすくするための、拡張されたandroidAPI。
パッケージ名に規則があって androidx.xxx.xxx.fragment → androidx.xxx.xxx.fragment-ktx となっている。
正直本当にリーダブルか疑わしいので、上方互換性が心配だけれども、細かいことを抜きにすれば、(-ktx)があるパッケージならとりあえずそれ使っておけばいいと、私は解釈した。

ADV

めっっっっっっっっちゃ早くなっているので、公式のADV使っておけばOKになった。
google playと書いてあるイメージは、non rootでgoogle play storeがプリインストールされていて、実際の商品のデバイスに近いイメージ。ソシャゲとかもできるものは普通できる。
google apiと書いてあるのはrootでgoogle play storeは入っていないらしい(使う理由がないので使っていない)

アーキテクチャ

https://developer.android.com/jetpack/guide?hl=ja
MVVMとかfluxとかアレ系の波がandroidにも来ている。iOSと違って公式フレームワークとしてこういうデザインパターンを強制するものをガシガシ出すのがAndroidっぽい。
こういうのを使わなくてもアプリは作れるけど、ある程度複雑な画面があると、こういうのを使ったほうが良いのは経験上知っているので、逃げずに使おうと思いました。

サポートライブラリとは

新しいバージョンのSDK?android-api?で出た機能を古いバージョンでも動くようにするためのものです。従来はandroid.supportというネームスペースで出てたそうな。appcompatとかもそれの一部だそうな。…多分。


以下詰まったことを無限にメモります

Fragment#onActivityCreated(Bundle?): Unit is deprecated

ボイラープレートや、アーキテクチャガイドでも普通に使ってるのに言われる。どうすれば良いのかIDEは教えてくれない。
ググる
https://developer.android.com/reference/androidx/fragment/app/Fragment#onActivityCreated(android.os.Bundle)

どうやらこのコールバックを使うこと自体が駄目らしい。onViewCreatedやonCreated使っとけとのこと。

で、地雷があって、ぐぐるとネイティブのFragmentのリファレンスやら昔のサポートライブラリのFragmentのリファレンスが引っかかる。しっかりとandroidxのFragmentのAPIリファレンスであることを確認するべし。

viewModels() 使ったら、 Cannot inline bytecode built with JVM target 1.8 into bytecode that is being built with JVM target 1.6. Please specify proper '-jvm-target' option

アーキテクチャガイドに従い

class MainFragment : Fragment() {
    private val viewModel: MainViewModel by viewModels()

と書いたら`viewModelsの出ました。

https://stackoverflow.com/questions/48601549/why-kotlin-gradle-plugin-cannot-build-with-1-8-target

を参考にして

appのbuild.gralde
android {
+    kotlinOptions {
+        jvmTarget = '1.8'
+    }
}

で解決。
もうこの時点でAndroidのガイドの品質がかなり絶望的になっている。

android-studioがtooltipで出すエラーメッセージコピペできない

この記事を書くにあたって詰まった。
https://stackoverflow.com/a/43665444/13639556
によると、なんか…こうShiftキーをうまく押しながらやればコピーできるらしい。できた。

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