20200629のAndroidに関する記事は8件です。

Splash画面を実装する方法

はじめに

Splash画面とは、アプリ起動時に最初に表示される画面です。アプリを起動するのに少なからず時間がかかります。その間に差し込む画面です。

Splash画面は、↑のGIFで写っているドロイド君の画面です。
この画面を作成する方法は大きく分けて2つあります。
1つ目は、Activityのテーマを変更する方法
2つ目は、Splash画面ようにActivityを作成する方法です。
今回はそれぞれの長所短所と実装方法についてまとめていきます。

1つ目の方法

1つ目は、Activityのテーマを変更する方法です。アプリは起動すると、デフォルトではwindowBackgroundの画面が表示されるらしいです。このプレースホルダーをSplash画面に拡張します。

実装

windowBackgroudcolorに、これから作成するSplash画面を設定します。

styles.xml
...
    <style name="AppTheme.Splash" parent="Theme.AppCompat.DayNight.NoActionBar">
        <item name="android:windowBackground">@drawable/splash_drawable</item>
    </style>
...

今回用意したSplash画面は、中心にドロイド君が配置された画面になります。それを実装すると、

splash_drawable.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list
    xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:drawable="@color/colorWhite" />
    <item
        android:drawable="@drawable/ic_baseline_android_24"
        android:gravity="center"
        />

</layer-list>

このようになります。真ん中に配置するドロイド君は、

New -> VectorAssetから作成してください。

起動時のテーマをAppThemeからApptheme.Splashに変更します。

AndroidManifest.xml
<activity android:name=".MainActivity"
            android:theme="@style/AppTheme.Splash">

アプリが起動する準備ができたら、MainActivityのsetContentView()で渡されたレイアウトが適応されます。その前にThemeを元のAppThemeに変更しましょう。

MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setTheme(R.style.AppTheme)
        setContentView(R.layout.activity_main)
    }
}

以上の実装でドロイド君が中心に配置してあるSplash画面を作成することができました。

長所
この実装方法では、アプリが起動する準備ができ次第、Splash画面から遷移するのでユーザーの時間を無駄にすることがありません。さらに、アプリがすでにメモリにある場合はあまり表示されることもないです。
短所
短所?ではないかもしれませんが、この実装だとアニメーションのような画面を表示することができません。

2つ目の方法

2つ目は、Splash画面ようにActivityを作成する方法です。この方法だと、1つ目の方法でできなかったアニメーションや複雑な画面を実装することができます。

実装

この方法では、様々なやり方があると思いますが今回はCoroutineを使って実装します。
まず、Gradleに依存関係を記述します。

build.gradle
dependencies {
    //Coroutine
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
}

新たにSplashActivityというActivityを作成します。

SplashActivity.kt
class SplashActivity : AppCompatActivity() {

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

        CoroutineScope(Dispatchers.Main).launch {
            delay(2000)
            val intent = Intent(this@SplashActivity, MainActivity::class.java)
            startActivity(intent)
            finish()
        }
    }
}

このActivityでは2秒のdelayの後、MainActivityに遷移するという実装をしてあります。Splash画面用にActivityを作成することにより、レイアウトにアニメーションを追加したり、様々なことができます。今回はプログレスバーを表示させるSplash画面を作成しました。

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

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/splash"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Slash"
        android:textSize="40dp"
        app:layout_constraintBottom_toTopOf="@id/progressbar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        />

    <ProgressBar
        android:id="@+id/progressbar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/splash"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

AndroidManifestに、起動時のActivityをSplashActivityに指定してあげれば完了です。

AndroidManifest.xml
        <activity android:name=".SplashActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".MainActivity" />

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

Apps Up 2020: アプリコンテストのご案内

革新的なアプリの作成、Huaweiモバイルサービスでアプリを最適化

Apps Up 2020ーHuawei HMSアプリイノベーションコンテストはHuaweiのDigiX Geekフランチャイズの一部です。
DigiX GeekフランチャイズはHMS Coreオープン機能の促進、HMS コアキットの導入と使用による革新的なアプリの作成、170以上の国と地域の6億人のHuaweiデバイスユーザーへのより優れたスマートライフサービスの提供を促す、グローバルプログラムです。
DigiX Geekプラットフォームは、HMSに根ざしたグローバルモバイルエコシステムを革新し構築するために、開発の才能と愛好家の新時代を統合することを目的としています。

image.png
image.png
image.png
image.png

登録参加対象者

  • APAC地域に拠点を置くすべてのモバイルアプリ開発者は、Apps Up 2020 – APACに登録して参加することができます。
  • 学生でも、趣味でも、プロでも、新興企業、中小企業、多国籍企業など、どなたでもご参加いただけます。
    • 既存のAndroidアプリがある。
    • Huawei AppGallery用のまったく新しいアプリを作成する予定。
    • Huawei AppGalleryでアプリを公開またはランチしている。

基準

  • コンテストに提出するアプリは、Huawei HMSオープン機能を活用し、少なくとも1つのHMSコアキットを導入する必要があります(HMSコアキットの詳細については、こちらを参照してください)
  • アプリは、HMSがインストールされている端末で完全に実行する必要があります。

ルール

  • 個別にコンテストに参加するか、最大3人のメンバーで構成されるチームを構成できます。
  • すべてのメンバーは、コンテストへの参加の一環として、Huawei IDを介してHuawei Developersにサインインする必要があります。 Huawei Developerアカウントをまだ持っていない場合は登録して、コンテストに参加する前に本人確認に合格するよう求められます。
  • 各参加者は1つのチームにしか参加できないことに注意してください。 また、各チームは、メンバーの招待/削除、作品の提出など、オンラインでのチームメンバーの管理管理を担当するチームリーダーを特定します。
  • 各個人は、チームへの参加または脱退を申請できます。
  • 各個人またはチームは、Huawei Developers契約またはHuawei AppGalleryポリシーに完全に準拠する必要があります。
  • チームのコンテストリージョンは、チームリーダーがHuawei Developersで開発者として登録するリージョンに従います。
  • 所有者の事前の承認の下で、提出されたアプリはサードパーティのゲームエンジン、ミドルウェア、オープンソースソフトウェア、またはコードライブラリを使用する場合があります。

失格となるシナリオ

  • 提出されたアプリが1つもHMS Coreキットを導入していない場合。
  • 盗作。
  • サードパーティのコードプラグインの使用許可を取得できませんでした。
  • 提出されたアプリは、Huawei Developers契約またはHuawei AppGalleryポリシーに違反していることが判明しました。

登録方

image.png

作品提出

  • チームリーダーは、イベントのウェブサイトに記載されている規定の期限までに作品を提出する必要があります。
  • 作品提出は2つのステップで構成されています:
    1. AppGallery Connectを使用してアプリを作成すると、アプリIDが発行されます。 Huawei AppGalleryへのアプリの作成と送信については、こちらをご覧ください。
    2. チームリーダーは、Apps Upイベントチームページに進み、コンテスト提出要件を完了します。
      • アプリ名
      • アプリID
      • アプリの説明
      • 仕事の添付ファイル–アプリの.apkファイル、仕事の紹介ドキュメント、およびアプリの機能と操作を紹介するマーケティングビデオを含める必要があります(ZIPまたはRAR形式である必要があり、200 MBを超えることはできません。例外はすべて、appsup.apac@huawei.comまでご連絡ください)
  • 各チームは最大5つのアプリを提出できます。

評価基準

image.png

報酬と受賞カテゴリー

image.png

HMSについて学ぶ

実際端末の使用に基づく自動テストプロセスの完全なセット。 これにはHuawei Developer consoleか らアクセスできます。

  • APAC 地域のオンラインワークショップ

APACリージョナルオンラインワークショップを通じて、HMSコアキットと導入するための個別のトレーニングを受けます。 appsup.apac@huawei.comで関心を示していただければ、ご連絡いたします。

image.png

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

Realmの基本的な使い方まとめ in Kotlin

1.概要

RealmはSQLiteに代わる軽量、高速データベースでアプリ開発にも利用できます。
そのRealmが7.0.0になりKotlinの正式ドキュメントも公開されました!
せっかくなのでAndroidアプリでの簡単な使い方をまとめます。
7.0.0はbeta版で、安定板は6.0.2になります。お好みでどうぞ!

■URL
【公式】Kotlinドキュメント
https://realm.io/docs/kotlin/latest/
【公式】Javaドキュメント
https://realm.io/docs/java/latest/
自作サンプル(INSERT,SELECT,DELETEしているだけ)
https://github.com/KIRIN3git/RealmTest

2.事前準備

build.gradleは7.0.0と6.0.2で設定が違います。

build.gradle【7.0.0】
buildscript {
    repositories {
        jcenter()
        google()
        maven {
            url 'http://oss.jfrog.org/artifactory/oss-snapshot-local'
        }
    }
    dependencies {
        classpath "io.realm:realm-gradle-plugin:7.0.0-beta-SNAPSHOT"
    }
}

allprojects {
    repositories {
        jcenter()
        google()
        maven {
            url 'http://oss.jfrog.org/artifactory/oss-snapshot-local'
        }
    }
}
build.gradle【6.0.2】
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath "io.realm:realm-gradle-plugin:6.0.2"
    }
}
[app]build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'realm-android'

(注意)kotlinプラグインの後にrealm-androidでないとダメ

3. 初期化と設定

初期化と設定を行います。
初期化の場所はどこでもOKですが、最初に呼び出せば良いのでApplicationクラスがオススメだそうです。

設定はDBを分けたり、拡張したり、永続化せずインメモリにしたり、リードオンリーにしたりするのに利用するそうです。
設定しなくても動きます。
・参考
RealmConfiguration

MainApplication.kt
class MainApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // 初期化
        Realm.init(this)

        // 設定
        val config = RealmConfiguration.Builder()
            .name("myrealm.realm")
            .encryptionKey(getMyKey())
            .schemaVersion(42)
            .modules(MySchemaModule())
            .migration(MyMigration())
            .readOnly()
            .inMemory()
            .build()
        Realm.setDefaultConfiguration(config)
    }
}

4. モデルクラスの作り方

データをinsertしたりselectするためのデータの型を作成

  • 手順
    • RealmObjectを継承(必須)
    • openに設定(必須)
    • 必要に応じてアノテーションを設定

プライマリーキー​を設定するとcopyToRealmOrUpdateまたはinsertOrUpdateを使用できます。

Person.kt
open class Person(
    @PrimaryKey var id: Long = 0,
    var name: String = "",
    var age: Int = 0
) : RealmObject()

5. 登録方法(INSERT)

  • 手順
    • インスタンスを作成
    • トランザクションでラップ
    • insert
    • インスタンスを閉じる

インスタンスはアクティビティ内などで使いまわしても良いです。
そしてインスタンスは必ず閉じましょう!
トランザクションの方法は色々あります。
非同期で使いたい場合はexecuteTransactionAsync

RealmSql.kt
// インスタンス作成
var realm = Realm.getDefaultInstance()

val personData = Person(1,Taro,15)

realm.beginTransaction()
realm.insert(personData)
realm.commitTransaction()

// インスタンスを閉じる
realm.close()

6. 取得方法(SELECT)

  • 手順

    • インスタンスを作成
    • データの取得
    • 絞り込み
    • メモリ外にコピー
    • インスタンスを閉じる
  • 主な取得関数

    • findAll:クエリ条件を満たすすべてのオブジェクトを検索します
    • findAllAsync:バックグラウンドスレッドで非同期に動作します
    • findFirst(またはfindFirstAsync):クエリ条件を満たす最初のオブジェクトを見つける

その他関数はこちら(Javaだけど・・・)
https://realm.io/docs/java/3.5.0/api/io/realm/RealmQuery.html

利用方法はこちらを参照
https://realm.io/docs/kotlin/latest/#queries

RxJavaも利用できる!(試してはいない・・・)
https://realm.io/docs/kotlin/latest/#rxjava

(注意) realmオブジェクトはRealmインスタンスをクローズすると消えてしまう。kotlinオブジェクトとして使いたい場合はcopyFromRealmを使用!

RealmSql.kt
// インスタンス作成
var realm = Realm.getDefaultInstance()

// 全取得
val users = realm.where<Person>().findAll()
// 絞り込み
users.equalTo("name", "John”)
// メモリ外にコピー
val user = realm.copyFromRealm(users[0])

// インスタンスを閉じる
realm.close()

7. 削除方法(DELETE)

  • 手順
    • インスタンスを作成
    • トランザクションでラップ
    • delete
    • インスタンスを閉じる
RealmSql.kt
// インスタンス作成
var realm = Realm.getDefaultInstance()

realm.beginTransaction()
// 削除
persons.deleteAllFromRealm()
realm.commitTransaction()

// インスタンスを閉じる
realm.close()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Facebookアプリからのディープリンク【Android編】

はじめに

※本記事では通常のwebなどからのディープリンクについては記載を割愛しますm(_ _)m
前回の記事に続き、今回はAndroidのFacebookアプリからディープリンクについてを記事にしたいと思います。
有料(従量課金)のツールとか使えば楽できるみたいなのですが、今回のミッションはFacebookアプリからだけなのでお金をかけずにとのことでした。
ので自前で実装することになったんですが…かなり苦戦しましたので記事にして残しておきたいと思います。

実現方法

どのように実現するか調べたらまず公式のApp Links on Androidが見つかりました。
早速導入して試したんですが、全く機能せず…色いろ調べても解決方法が見つけられず…
一旦諦めました(><)

が、ベテラン技術者の方に色々調べてもらいサーバ側にプログラムを仕組んだらできるのでは無いか?ということで試していただくことに。

導入したのは

の2つを導入しました。
が、おそらくApp Links on Androidだけで動くと思われますのでChromeのintentは概要だけ記載します。

Chromeのintent

App Links on Android以外にもintent起動する方法があって最初はこれを実装していました。
以下が構成イメージです。meta情報を加工してリダイレクトするだけの割とシンプルな構成です。

image.png
ただ古い機種(ASUS Zenfone4:Android8)ならうまくいくんですが、比較的新しい(Android10とか)の機種ではなぜかうまくいきませんでした。原因はわからないまま…
なので次項のApp Links on Androidの導入に至りました。

App Links on Android

こちらが本題。以下のようなイメージ構成です。
iOSの時と大差無いですが、違っているのはサーバ側にプログラムを仕組んだことです。
通常のmetaデータだけだとFacebookアプリがうまくintent起動してくれないのでサーバ側でmetaデータを加工してあげることになりました。
image.png
Androidの場合は①③④をFacebookアプリがやってくれます。

サーバ側

Facebookアプリからのディープリンク【iOS編】の時と同じようにサーバ側にページを用意し
[https://hogehoge.com]
にアクセスがきたら用意したページに飛ぶようにサーバ側で設定します。
今回はmetaデータの加工が必要なのでphpファイルを準備しました。

そして、そのphpでmetaデータを加工する処理を仕組みます。
※パラメータの解説はApp Linksの技術文書を和訳してみましたが参考になりました。

<?php

// 定数定義
define('ANDROID', 0);
define('IOS', 1);
define('OTHER_OS', 999);
define('FACEBOOK', 0);
define('OTHER_BROWSER', 999);
define('APP_NAME', 'あなたのアプリの名前');
define('ANDROID_STORE', 'https://play.google.com/store/apps/details?id=あなたのアプリID');
define('ANDROID_SCHEME', 'hogehoge-app');
define('ANDROID_CUSTOM_URL_HOST', 'hogehoge-link');
define('ANDROID_PACKAGE', 'あなたのアプリのパッケージ名');

// サーバ変数からホスト、url、ユーザエージェント取得
$httpHost = $_SERVER[HTTP_HOST];
$requestUri = $_SERVER[REQUEST_URI];
$uerAgent = $_SERVER[HTTP_USER_AGENT];

header("Content-type: text/html; charset=utf-8");

$os = findOS($uerAgent);
$browser = findFacebook($uerAgent);
if($os === ANDROID || $browser === FACEBOOK) {
    $host = ANDROID_CUSTOM_URL_HOST;
    $scheme = ANDROID_SCHEME;

    // metaデータに出力するlink
    $androidDeeplink = "$scheme://$host$requestUri";
}

function findOS($uerAgent) {
    if(preg_match('/Android/ui', $uerAgent)) {
        return ANDROID;
    } else if(preg_match('/iPhone|iPod|iPad/ui', $uerAgent)) {
        return IOS;
    }
    return OTHER_OS;
}

function findFacebook($uerAgent) {
    if(preg_match('/FB_IAB/ui', $uerAgent)) {
        return FACEBOOK;
    } else if(preg_match('/facebook/ui', $uerAgent)) {
        return FACEBOOK;
    }
    return OTHER_BROWSER;
}
?>

<html>
<head>
    <meta property="al:web:should_redirect" content="false" />
    <meta name="viewport" content="width=device-width,initial-scale=1">

    <meta property="al:android:package" content="<?php echo ANDROID_PACKAGE ?>" />
    <!-- ここで加工したlinkを出力。このmetaタグでFacebookアプリが動いてくれる -->
    <meta property="al:android:url" content="<?php echo $androidDeeplink; ?>" />
    <meta property="al:android:app_name" content="<?php echo APP_NAME ?>" />

    <!-- キャッシュは無効にしておく(でも、多分OGP情報のキャッシュには無力と思われる) -->
    <meta http-equiv="cache-control" content="max-age=0" />
    <meta http-equiv="cache-control" content="no-cache" />
    <meta http-equiv="expires" content="-1" />
    <meta http-equiv="expires" content="Tue, 31 May 2011 10:15:00 GMT+3" />
    <meta http-equiv="pragma" content="no-cache" />

</head>
<body style="font-size: medium;">

<!-- アプリがインストールされていない場合ストアに遷移させる -->
<?php
    if($os === ANDROID || $browser === FACEBOOK) {
?>
    <script>
        window.onload = function() {
            setTimeout(function() {
                window.location = "<?php echo ANDROID_STORE; ?>";
            }, 3000);
        };
    </script>
<?php
    }
?>
</body>
</html>

※自分のAndroidはPlay Storeで探しましょう。
※Androidの説明なのでiOSの記載は省略しています。iOSも同じurlを使う場合はFacebookアプリからのディープリンク【iOS編】の処理をphpファイルに移植してください。

アプリ側

まずはintent起動できるようにAndroidManifest.xmlに追記します。

        <activity android:name=".DeepLinkActivity"
            android:launchMode="singleTop"
            android:noHistory="true"
            android:screenOrientation="portrait"
            android:theme="@style/AppTheme.NoActionBar">
            <!-- app linkとfacebookアプリからのアプリを起動は共存できないのでapp linkは使わない。 -->

            <!-- deep link for facebook app -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <!-- dataタグのschemeとhostはphpで定義したANDROID_SCHEMEとANDROID_CUSTOM_URL_HOSTに合わせます -->
                <data android:scheme="hogehoge-app" android:host="hogehoge-link" />

                <!-- リンクさせるパスを追記します -->
                <data android:pathPattern="/top" />
                <data android:pathPattern="/setting" />

            </intent-filter>
        </activity>

次にディープリンクを制御するアクティビティを準備します

import android.content.ComponentName
import android.content.Intent
import android.net.Uri
import android.os.Bundle

class DeepLinkActivity {

    companion object {
        private const val PATH_TOP = "/top"
        private const val PATH_SETTINGS = "/setting"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val intentFromDeepLink = intent
        /**
         取れるのはこんな情報
         Intent { 
           act=android.intent.action.VIEW 
           dat=hogehoge-app://hogehoge-link/top&target_url=https://hogehoge.com/top
         }
         **/

        val newIntent = Intent(Intent.ACTION_MAIN)
        newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

        var originalUrl: String? = null

        // facebookアプリからurl情報を受け取り
        intentFromDeepLink?.let { facebookIntent ->
            val uri = facebookIntent.data
            /**
             uriはこんな情報
             hogehoge-app://hogehoge-link/top&target_url=https%3A%2F%2Fhogehoge.com%2Ftop
             **/
            val facebookUrl = uri?.getQueryParameter("target_url")
            facebookUrl?.let {
                originalUrl = it
                /**
                 originalUrlはこんな情報
                 https://hogehoge.com/top
                 **/
            }
        }

        // urlを元に起動する画面のintentを設定する
        originalUrl?.let {
            val uri = Uri.parse(it)
            setIntent(uri, newIntent)
        }

        startActivity(newIntent)
    }

    private fun setIntent(uri: Uri?, intent: Intent) {
        if (uri == null) {
            setIntentToTop(intent)
            return
        }

        when (uri.path) {
            PATH_TOP -> {
                setIntentToTop(intent)
            }
            PATH_SETTINGS -> {
                setIntentToSetting(intent)
            }
            else -> {
                setIntentToTop(intent)
            }
        }
    }

    private fun setIntentToTop(intent: Intent) {
        val cls = TopActivity::class.java
        intent.component = ComponentName(packageName, cls.name)
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
    }

    private fun setIntentToSetting(intent: Intent) {
        val cls = SettingActivity::class.java
        intent.component = ComponentName(packageName, cls.name)
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
    }
}

これでFacebookアプリからのディープリンクができるようになったと思います。
iOSと同じようにOGP情報のキャッシュがあるので、サーバ側を更新したらOGPキャッシュのクリアは必須だと思っていいです。

終わりに

私一人では解決できなかったですが、ベテラン技術者の方に協力いただいてなんとか機能させることができました。大感謝です。
Facebook公式に書いてある情報だけでは実現できないって…ちょっと不親切かな〜と思いました。
他のアプリの中身はわからないので手探り状態でしたがとってもいい経験になったと思います。

日本語の記事は見当たらなかったので誰かのお役に立てたらなと思います。

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

KotlinでTCP/IP送信

はじめに

AndroidでTCP/IP通信をしようとするとうまくいかないので、その対応方法の記録です。
2018年ごろに作られているTCP/IPのサンプルがまったく動作しないのでその原因を調査しました。

サンプル1

MainActivityのボタンクリックイベント内に

sample1.kt
        var socket : Socket? = null
        try {
            val socket = Socket("localhost", 12345)
            val writer : PrintWriter  = PrintWriter(socket.getOutputStream(), true)
            writer.println("serial test")
        }
        if (socket != null) {
            socket.close()
        }

と代表的なサンプルを書いて動作させてみましょう。
コメントの方にすばらしく簡潔なサンプルがありますのでそちらも参考にして下さい。
これは動作しません。動作しないと言うより接続に行きません。
エラー内容を見るとパーミッションを指定して下さいと表示されるのでAndroidManifest.xmlを修正します。

AndroidManifest.xml
    <application
        中略
        android:usesCleartextTraffic="true"
        中略
    </application>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

INTERNETの指定がインターネットへ接続可能にする設定
ACCESS_NETWORK_STATEはネットワークの様々なやりとりを有効にする設定
android:usesCleartextTraffic="true"は暗号化無しの通信を許可する設定です。

INTERNET以外の設定は不要と思いますが今後のバージョンアップでまた悩むのが嫌なのであらかじめ付けてます。

そして実行すると今度はアプリが落ちます。
エラーメッセージに若干のヒントはあるのですが要は「ボタンクリックイベントから呼ぶな」と言っているようです。

サンプル2

じゃあ簡単なスレッドを作ってその中で動作させてみます。
コメントの方にすばらしく簡潔なサンプルがありますのでそちらも参考にして下さい。

sample.kt
        val runnable = object : Runnable {
            override fun run() {
                var socket : Socket? = null
                try {
                    socket = Socket("localhost", 12345)
                    val writer : PrintWriter  = PrintWriter(socket.getOutputStream(), true)
                    writer.println("serial test")
                } finally {
                    if (socket != null) {
                        socket.close()
                    }
                }
            }
        }
        val thread = Thread(runnable)
        thread.start()

今度は動作します。

まとめ

元々接続処理を行うと指定しても一定時間処理が返ってこないのでメインで処理する物ではありませんが、だからといってアプリごと落とさなくても・・・

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

Androidエミュレータ(Windows)で使用するシステムイメージを差し替える・/systemの内容を変更する

はじめに

Android Studioでアプリ開発するにあたり、SDK Managerでエミュレータとシステムイメージを取得して使用していましたが
・自前でAOSPからビルドしたシステムイメージを使う
・エミュレータで/system以下に変更を加える(アプリをpushするなど)
という必要があり、それで行き詰まった部分があるので、解決した方法と合わせて記録しておきます。

わたしの環境

  • Android Studio/エミュレータ動作環境

    • Windows 10 Home 64bit
    • CPU: Core i7-8700 @ 3.20GHz
    • RAM: 16GB
    • Android Studio: 3.6.3
    • Android Emulator: 30.0.12
  • AOSPビルド環境(仮想PC)

    • Oracle VM VirtualBox 6.0
    • Ubuntu 16.04 LTS
    • AOSPソースコード: Android Pie android-9.0.0_r42

自前でビルドしたシステムイメージを使いたい

困ったこと

エミュレータを使用する際はAVD Managerで何らかのデバイスを作成→システムイメージを選択します。
このシステムイメージはAndroid各バージョン(私の環境ではAPIレベル30~古いのは7), x86-32/64, Google Play有無など選ぶことができますが、自前でカスタマイズしたシステムイメージを使用したいということもあると思います。

私の場合はAndroid9に対し、ビルドオプション変更やプリインストールのアプリを追加したものを使いたかったので、AOSPでx86-64bit向けのシステムイメージ(GSI)を作成しました。
詳細な流れは省略しますが、Ubuntu上でビルド環境の構築→ソース取得→ビルド環境選択→ビルドです。
詳しくはこちらこちらこちらですね。

ビルドの流れ(ざっくり例)
$ sudo apt-get install git-core gnupg flex bison gperf build-essential zip curl zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 lib32ncurses5-dev x11proto-core-dev libx11-dev lib32z-dev libgl1-mesa-dev libxml2-utils xsltproc unzip
$ mkdir <作業ディレクトリとか>
$ repo init -u https://android.googlesource.com/platform/manifest -b android-9.0.0_r42
$ repo sync
$ source build/envsetup.sh
$ lunch aosp_x86_64-userdebug
$ make -j8

出力先ディレクトリ(/home/<user>/<workspace>/out/target/product/generic_x86_64)にこんなイメージ群ができあがりました:

$ ls | grep img
cache.img
encryptionkey.img
ramdisk.img
system-qemu.img
system.img
userdata.img
vbmeta.img
vendor-qemu.img
vendor.img

これで作成したシステムイメージをエミュレータで使用したかったのですが…AVD Managerでは自前で用意したシステムイメージを選択するようなメニューが無かったので困りました。

解決方法

エミュレータ起動時のオプションで

C:\xxx\Android\android-sdk\tools>emulator -system <自前でビルドしたイメージ置き場>/system-qmeu.img -avd xxx

でシステムイメージを参照することで、そのイメージを使用してエミュレータが起動します。
ポイントはsystem.imgではなくsystem-qemu.imgを使用することでしょうか。(system.imgだと起動しませんでした)
vendorも同様に -vendor <自前でビルドしたイメージ置き場>/vendor-qmeu.img のオプションを使用して自前のイメージが使えます。

起動オプションで指定するのが面倒な場合は、SDK Managerで取得したシステムイメージ置き場のファイルを直接置き換えれば済みます。自分はそうしてしまいました。

C:\xxx\Android\android-sdk\system-images\android-28\default\x86_64\
  system.img ←system-qmeu.imgをリネームして配置
  vendor.img ←vendor-qmeu.imgをリネームした配置

※注意
自前のシステムイメージを置き換えて使用する際は、置き換え前と後でAndroidのバージョンやアーキテクチャを合わせる必要があります。
SDK Managerでx86-32bit用で取得したシステムイメージに対し、x86-64bit用でビルドしたイメージを置き換えても正常に動作しない(はず)です。

/systemパーティションの内容を変更したい

困ったこと

自前でビルドしたシステムイメージでエミュレータを起動することはできましたが、それに追加で/systemパーティションにアプリやライブラリをpushしようとしてもRead-Onlyでできません。
よくある解決方法として、/systemをリマウントするというものがありますが、どうにもうまくいきません。

mountコマンド
C:\>adb shell
$ mount -o,rw remount /system
$ mount: 'remount'->'/system': No such file or directory
adbコマンド
C:>adb root
C:>adb remount
remount of the / superblock failed: Permission denied

解決方法

Android公式に解決できる起動オプションが載っていました。

-writable-system
エミュレーション セッション中に書き込み可能なシステム イメージを作成する際にこのオプションを使用します。方法は次のとおりです。
 1. -writable-system オプションを使用して仮想デバイスを起動します。
 2. コマンド ターミナルから adb remount コマンドを入力し、system/ を読み取り / 書き込み用として再マウントするようにエミュレータに指示します(デフォルトでは読み取り専用としてマウントされます)。

writable-systemで起動
C:\xxx\Android\android-sdk\tools>emulator -writable-system -avd xxx
emulator: WARNING: System image is writable
...
emulator: INFO: boot completed
からのadbコマンド
C:>adb root
C:>adb remount
remount succeeded

すんなりいけました!

ここで/systemに加えた変更はエミュレータを落としても保持されます。
次回、また-writable-systemオプションで起動すれば前回の変更内容が反映された状態です。
ただ-writable-systemなしで起動した際は、/systemの変更なしの初期状態となります。(そしてRead-Onlyです)
また通常Read-Onlyなのは/systemだけでなく/vendorなども同様ですが、-writable-systemでエミュレータを起動すると、/vendorなども変更が可能となります。

なお各AVDの設定やユーザーデータの類は以下の場所に置かれていますが、デフォルトの状態に対して加えた変更は"xxx.img.qcow2"ファイルに書かれているようです。
AVD ManagerなどでWipe Dataするとすべての変更はなくなります。

C:\Users<user>.android\avd\xxx.avd\
  userdata-qemu.img.qcow2
  system.img.qcow2
  vendor.img.qcow2
  …

userdata-qemu.img.qcow2は-writable-systemなしで起動しても毎回エミュレータ終了時に更新されて保存されますが
system.img.qcow2, vendor.img.qcow2 などは-writable-system の場合のみ更新されることがわかります。

最後に

当然といえば当然ですが、実機とエミュレータとではいろいろと扱い方に違いがあるのだなと感じました。
また何か困りごと&解決方法が出てきたら追記しようと思います。

以上です。

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

Flutterで画像、カスタムフォントを使用する方法

今回はFlutterアプリで画像とカスタムフォントを使用する方法を記述します。

プロジェクトに画像とフォントを追加する

  1. プロジェクト直下にassetsフォルダを作成し、その直下にimagesfontsフォルダを作成する。
  2. 使用したい画像とフォントを各フォルダに格納する。

スクリーンショット 2020-06-29 4.11.29.png

pubspec.yamlにパスを記述する

Flutterで画像やフォントを使用するには、pubspec.yamlにパスを記述する必要があります。

pubspec.yaml
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  assets:
    - assets/images/
    - assets/fonts/

# An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware.

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  fonts:
    - family: OpenSansCondensed
      fonts:
        - asset: assets/fonts/OpenSansCondensed-Light.ttf
          weight: 300
        - asset: assets/fonts/OpenSansCondensed-Bold.ttf
          weight: 700

フォントファイルは太さによって複数に分かれていることが多いので、それぞれにweightを定義します。
pubspec.yamlに記述する際、インデントが揃っていないと、エラーになったり、上手く適用されないなどがあるので、注意が必要です。

画像を表示する

実際にアプリ上にプロジェクトに追加した画像を表示してみます。

aseetsの画像を表示する

main.dart
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter Demo'),
        ),
        body: Center(
          child: Image.asset('assets/images/sample.png'),
        ),
      ),
    );
  }
}

ビルドすると以下のように画像を表示させることができました。
Simulator Screen Shot - iPhone 11 Pro - 2020-06-28 at 21.53.33.png

ネットワーク上の画像を表示させる

Image.network('イメージのURL')

画像のURLを記述することで、URL先の画像を表示させることもできます。

カスタムフォントを使用する

カスタムフォントを適用する方法は主に2種類あります。

アプリ全体のデフォルトに設定する

MaterialApp()themeに指定してあげることで、デフォルトのフォントに設定されます。

main.dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(fontFamily: 'OpenSansCondensed'),
      home: SamplePage(),
    );
  }
}

個別に指定する

特定のTextにのみ適用させたい場合は、以下のようにTextStylefontFamilyに指定します。
fontWeightpubspec.yamlに記述したweightを指定することで、それぞれのフォントファイルを使用することができます。

main.dart
Text(
          'custom font',
          style: TextStyle(
            fontFamily: 'OpenSansCondensed',
            fontWeight: FontWeight.w300,
            fontSize: 40,
          ),
        ),

Simulator Screen Shot - iPhone 11 Pro - 2020-06-29 at 04.16.40.png

おまけ

フォントファイルをわざわざ用意しなくとも、google_fontsを使用することができるFlutterのパッケージがありますので、そちらを利用してみるのもいいかもしれません。
https://pub.dev/packages/google_fonts

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

Dagger Hilt触ってみた

はじめに

巷で噂のDagger Hiltをようやく触ってみました。Codelabを一通り終わったので、実際に過去の記事でDaggerを使ってDIしたのですがそれをHiltに移行した手順をまとめます。

Gradle

build.gradle
    // build.gradle (Project)
    ext.hilt_version = '2.28-alpha'
    dependencies {
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }

build.gradle
    // build.gradle (App)
    // Dagger_hilt
    implementation "com.google.dagger:hilt-android:$hilt_version" // 必須
    kapt "com.google.dagger:hilt-android-compiler:$hilt_version" // 必須

    def dagger_hilt_view_model_version = "1.0.0-alpha01"
    implementation "androidx.hilt:hilt-lifecycle-viewmodel:${dagger_hilt_view_model_version}"
    kapt "androidx.hilt:hilt-compiler:${dagger_hilt_view_model_version}"

今回は、ViewModelを使うので、hilt-compilerhilt-lifecycle-viewmodelを追加した。

実装

移行の順番は、
1. Application & @Singleton
2. Fragment & Activity

ということで、Applicationを見てみましょう。

Application

Hiltに移行する前のコードがこちらです。Applicationでは、独自に設計したComponentをインスタンス化していました。しかし、Hiltでは標準のComponentが用意されていてComponentを定義する必要がなくなったらしい。

TodoApplication.kt
class TodoApplication : Application() {
    companion object {
        lateinit var component: AppComponent private set
    }

    override fun onCreate() {
        super.onCreate()
        component = DaggerAppComponent.factory().create(applicationContext)
    }
}

HiltではApplication@HiltAndroidAppアノテーションをつけて定義します。

TodoApplication.kt
@HiltAndroidApp
class TodoApplication : Application() {
//    companion object {
//        lateinit var component: AppComponent private set
//    }
//
//    override fun onCreate() {
//        super.onCreate()
//        component = DaggerAppComponent.factory().create(applicationContext)
//    }
}

このコードからわかるようにComponent自体もいらないので、AppComponentを削除します。

AppComponent.kt
//@Singleton
//@Component(
//    modules = [
//        DatabaseModule::class
//    ]
//)
//interface AppComponent {
//
//    @Component.Factory
//    interface Factory {
//        fun create(@BindsInstance context: Context): AppComponent
//    }
//    fun mainViewModel(): MainViewModel
//}

Activity

元々あったComponentを削除したので、ActivityやFragmentにInjectできるように、@AndroidEntryPointアノテーションを記述しなければならない。

MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

今回は、Fragmentが登場しませんが、もしFragmentにInjectしたいときはそれを所持するActivityにも@AndroidEntryPointを付ける必要があります。

Module

Moduleの定義方法は、@InstallIn(ApplicationComponent::class)アノテーションをつける必要があります。@InstallInアノテーションでHiltが生成するComponentにModuleを紐付けます。

DatabaseModule.kt
@InstallIn(ApplicationComponent::class)
@Module
class DatabaseModule() {


    @Singleton
    @Provides
    fun provideTodoDatabase(@ApplicationContext context: Context) =
        Room.databaseBuilder(context,
            TodoDatabase::class.java,
            "database_name")
            .build()

    @Singleton
    @Provides
    fun provideTodoDao(todoDatabase: TodoDatabase) = todoDatabase.todoDao()
}

移行前は、ContextをComponentのインスタンス生成する時に渡していたがそれをする必要がなくなりました。 すでにBinding済みのContextを使用することができます。@ApplicationContext@ActivityContextをつけるだけで使用できます。

ViewModel

ViewModelをHiltを使ってDIする場合、constructorに@ViewModelInjectアノテーションをつけます。

MainViewModel.kt
class MainViewModel @ViewModelInject constructor(private val repository: TodoRepository) : ViewModel() {

最後Activityに、

MainActivity.kt
private val mainViewModel: MainViewModel by viewModels()

を記述すれば移行完了です。

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