20200314のAndroidに関する記事は12件です。

Andorid Studioのインストールガイド

目次

  • インストーラのダウンロード
  • インストール
  • Android Studioの初期設定

インストーラのダウンロード

以下のリンクからAndroid Studio 3.6.1のインストーラをダウンロードする。
https://developer.android.com/studio

インストール

  • ダウンロードしたインストーラをクダブルリックする。
    0002.JPG

  • Android Studioのインストールポップアップが表示されたら、「Next」をクリックする。
    0004.JPG

  • 「Next」をクリックする。
    0002.JPG

  • 「Next」をクリックする。
    0003.JPG

  • 「Install」をクリックする。
    0004.JPG

  • インストールしているのを待って、完了後「Next」をクリックする。
    0005.JPG
    0006.JPG

  • 最後は「Finish」をクリックする。
    0007.JPG

Android Studioの初期設定

  • 上記のインストール完了後、以下のインポート設定のポップアップが表示されたら、「OK」をクリックする。

0008.JPG

  • Android Studioがロードされる。
    0009.JPG

  • 「Next」をクリックする。
    0010.JPG

  • 自分の好きなテーマを選び、「Next」をクリックする。
    0012.JPG

  • 設定が完了後、「Finish」をクリックする。
    0013.JPG
    0014.JPG

  • Android Studioの必要なコンポーネントがインストールされる。
    0015.JPG

  • インストールが完了後、「Finish」をクリックする。
    0016.JPG

  • これでAndroid Studioの初期設定が完了で、アンドロイドアプリケーションが作成出来る状態になる。
    0017.JPG

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

Android開発初心者の自分が躓いたところ

その1 SDKの差異

  • Conflict with dependency 'com.android.support:support-annotations'. Resolved versions for app (26.1.0) and test app (27.1.1) differ

どうやらSDKのヴァージョンの差異が原因のよう。

以下をbuild.gradle(Module:app)のdependencies内に追記。

androidTestCompile 'com.android.support:support-annotations:27.1.1'
compile 'com.android.support.test:runner:1.+'

その2 エミュレーターが動かない

  • 色々解決方法があるようだが、実機で確認したほうが手っ取り早い。 動かなければ諦めろ。

その3 実機を認識しない

  • スマホ側から設定→デバイス接続→USBの設定を確認し、「充電のみ」が設定されているので 「ファイルを転送」or 「写真を転送」に切り替える。

 併せて、たまにSDKManagerから「Google USB Driver」をインストールし忘れることがあるので
 必ずインストールしておくこと。

その4 メインのプログラム(javaとかkotlinとか)でRがエラーになる

  • 結構単純で、レイアウトを編集するstring.xmlやstyle.xml上にエラーが1つでも存在すると、ソース上のRは全てエラーになってしまうので、そちらで原因を探る。設定したidがメインプログラムと紐づいていないことが結構あった。スペルミスが大半。

最後に

  • 環境構築で時間がかかり、実際のコードもいつものJavaより難しい気も。 Unityを用いて、ARアプリの開発もしてみたい。

参考にしたサイト

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

Github ActionsのAndroid CIでNDKを切り替える

概要

Github ActionsのAndroid CIを実行させた場合、Github Actionsで用意されているHost Runnerをそのままを使うと、virtual-environments/Ubuntu1804-README.md に記載されたバージョンのNDKが適用される。
例えば、2020年3月14日時点ではAndroid NDK 21.0.6113669となる。

ただ、場合によっては別のバージョンのNDKを利用したいケースもある。

NDKの切り替えに対応したandroid.yml

CircleCI Orb Registry - circleci/android を参考に、NDKを指定できるようにandroid.ymlを記述する。

なお、NDK_SHANDK_VERSIONは、こちらを参考に設定すればよい。

name: Android CI

on:
  pull_request:
    types: [opened]
  push:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: set up JDK 1.8
      uses: actions/setup-java@v1
      with:
        java-version: 1.8
    - name : install ndk
      run: | 
        sudo curl --silent --show-error --location --fail --retry 3 \
          --output /tmp/$NDK_VERSION.zip \
          https://dl.google.com/android/repository/$NDK_VERSION-linux-x86_64.zip

        sudo echo "$NDK_SHA /tmp/$NDK_VERSION.zip" > /tmp/$NDK_VERSION.zip.sha1

        sha1sum -c /tmp/$NDK_VERSION.zip.sha1

        sudo rm -rf $ANDROID_SDK_ROOT/ndk-bundle
        sudo unzip -q /tmp/$NDK_VERSION.zip -d $ANDROID_SDK_ROOT
        sudo mv $ANDROID_SDK_ROOT/$NDK_VERSION $ANDROID_SDK_ROOT/ndk-bundle
        sudo chown -R root:root $ANDROID_SDK_ROOT/ndk-bundle
        sudo rm -f /tmp/$NDK_VERSION*

        if [[ -d $ANDROID_SDK_ROOT/ndk-bundle && \
          -n "$(ls -A $ANDROID_SDK_ROOT/ndk-bundle)" ]]; then
          echo "Android NDK installed"
        else
          echo "Android NDK did not install successfully"
          exit 1
        fi
      env:
        NDK_SHA: fd94d0be6017c6acbd193eb95e09cf4b6f61b834
        NDK_VERSION: android-ndk-r19c
    - name: Build with Gradle
      run: ./gradlew build

Gist

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

Android+RS-232C通信

はじめに

まだまだ計測機器と通信はRS-232Cが現役です。
ある計測機器(USB-miniB/RS-232C)とAndroidでRS-232C通信できないかと相談がありました。
そこでUSB-miniB/RS-232C(変換ケーブル)で通信できるか調査しました。

先に結論を書くと、

  • USB-TypeC→USB-TypeA変換→USB-miniBでは計測機器が認識できない端末あり。
  • USB-TypeC→RS-232C変換ケーブル(ラトックシステム製)→RS-232CではUSB-TypeCの端末で認識できました。

調査環境

計測機器の認識調査

  • UsbManager. getDeviceList()でUSB機器を検索します。
  • 見つかったUSB機器情報をテキストに表示します。
  • USB機器(計測機器/変換ケーブル)が見つからない場合、認識しないと判断します。
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager

class MainActivity : AppCompatActivity() {
    private lateinit var usbManager:UsbManager

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

        usbManager = getSystemService(Context.USB_SERVICE) as UsbManager

        btnTest.setOnClickListener {
            // USB機器を検索してテキストに表示
            val devices = usbManager.deviceList
            var str = ""
            for (device in devices.values) {
                val name = device.deviceName
                str += "Name:${name}\n"
                str += "Vendor:{$device.vendorId}\n"
                str += "Product:{$device.productName}\n"
                str += "\n"
            }
            txtMsg.text = str
        }
    }
}

USB-miniBで調査

  • USB-microB/Type-C→USB TypeA変換ケーブル→計測機器(USB-miniB)とAndroid端末の組み合わせで、計測機器を認識できる端末/できない端末があった。
  • USB-Type-Cでは認識できる端末/できない端末があり、変換ケーブルと端末の選定が難しくなった。

計測機器を認識できる組み合わせ

  • USB-microB→USB-TypeA→USB-miniB
端末 OS 認識
Nexus7 6.0.1 O
Moto5G 7.0 O
  • USB-TypeC→USB-TypeA→USB-miniB
端末 OS 認識
Pixel3a 10.0 O

計測機器を認識できない

  • USB-TypeC→USB-TypeA→USB-miniB
端末 OS 認識
ZenfoneAR 7.0 X
Zenfone5 8.0.0 X

USB-TypeC→RS-232C変換ケーブル(RS-USB60FC)で調査

  • 下記の全ての端末で認識できました。
  • USB-TypeC→RS-USB60FC
端末 OS 認識
ZenfoneAR 7.0 O
Zenfone3 Deluxe 8.0.0 O
Zenfone5 8.0.0 O
Nexus5X 8.1.0 O
Essential Phone PH-1 10.0 O
Zenfone5Z 10.0 O
Pixel3a 10.0 O

おわりに

  • USB-TypeC→USB-TypeA変換→USB-miniBでは計測機器が認識できない端末あり。
  • USB-TypeC→RS-232C変換ケーブル(ラトックシステム製)→RS-232CではUSB-TypeCの端末で認識できました。
  • ラトックシステム製のRS-232C変換ケーブルは、USB-TypeA/miniB/microB/Type-Cのタイプが用意されているので、ターゲット端末に応じて使おうと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android]Flow と Retrofit を組み合わせたサンプルと解説

はじめに

Flow と Retrofit を組み合わせたて次のサンプルを作成します。
アーキテクチャは Google が推奨している MVVM で作成を進めます。

作成するもの アーキテクチャ
image.png img

TL;DR

  • Flow と Retrofit を連携するときは、戻り値が Flow となるように自分で実装する。
  • Room から取得した Flow は asLiveData で LiveData に変換できる。
  • Flow を LiveData に変換したあとは、通常の LiveData と同じで Observe して利用する。
  • MVVM で LiveData<T>Flow<T> を組み合わせるとこの構成になる。

image.png

Setup

アプリケーションの作成に必要となる、
Koin・Retrofit・Flow・Coilのライブラリをインストールする。

ライブラリ バージョン 説明
Koin 2.1.3 DIライブラリ
Retrofit 2.2.4 HTTPクライアントライブラリ
Coroutines 1.3.4 非同期処理やノンブロッキング処理を行うためライブラリ
Coil 0.8.0 画像を読み込むためのライブラリ
dependencies {
       ︙  
    def koin_version = "2.1.3"
    implementation "org.koin:koin-android:$koin_version"
    implementation "org.koin:koin-android-scope:$koin_version"
    implementation "org.koin:koin-android-viewmodel:$koin_version"
    implementation "org.koin:koin-android-ext:$koin_version"

    def coroutines_version = "1.3.4"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"

    def retrofit_version ="2.1.0"
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"

    def coil_version = "0.8.0"
    implementation "io.coil-kt:coil:$coil_version"

    def lifecycle_version = "2.2.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
}

Model

Retrofit を通して Qiita のユーザー情報を取得できるようにします。ユーザー情報は Qiita API の GET /api/v2/users/:user_id を利用して取得します。次のクラスを作成し、Retrofit を通して Qiita API を利用できるようにします。

役割 クラス名 役割
Entity User Qiita API のユーザー情報を定義するクラス
Service QiitaService Qiita API を利用するためのサービスクラス
Repository UserRepository QiitaService を利用してデータを取得するクラス

User

Qiita ユーザー情報を格納するためのデータクラスを定義する。

data class User(
    val description: String,
    val facebook_id: String,
    val followees_count: Int,
    val followers_count: Int,
    val github_login_name: String,
    val id: String,
    val items_count: Int,
    val linkedin_id: String,
    val location: String,
    val name: String,
    val organization: String,
    val permanent_id: Int,
    val profile_image_url: String,
    val team_only: Boolean,
    val twitter_screen_name: String,
    val website_url: String
)

QiitaService

Retrofit で Qiita API の GET /api/v2/users/:user_id を利用できるようにする。詳しくは Retrofit の公式ドキュメント を閲覧してください。

interface QiitaService {
    @GET("/api/v2/users/{user_id}")
    fun getUser(@Path("user_id") user_id: String): Call<User>
}

UserRepository

QiitaService から取得した Call<User> を UserRepository で Flow<User> に変換する。こんな感じで ViewModel からデータ取得をリクエストする際には、Call<User> ではなく Flow<User> を通して行うようにしてやる。

class UserRepository(private val service: QiitaService) {
    fun getUser(userId: String): Flow<User> {
        return flow {
            try {
                emit(service.getUser(userId).execute().body())
            } catch (e: Exception) {
                Log.e("UserRepository", "getUser error", e)
                emit(nullUser)
            }
        }.flowOn(Dispatchers.IO)
    }
}

ViewModel

ViewModel では Model から取得した Flow<T>LiveData<T> に変換し、 View が LiveData<T> を購読してデータを表示できるようにしておきます。Flow<T>asLiveData()LiveData<T> に変換できるので、asLiveData() を利用して変換してやります。

class MainViewModel(private val userRepository: UserRepository): ViewModel() {
    val user: LiveData<User> = userRepository.getUser("kaleidot725").asLiveData()
}

View

Koin

作成してきた ViewModel と Model を生成するため Koin の AppModule を定義する。
次の定義で QiitaService・UserRepository・MainViewModel を生成できるようにする。

val appModule = module {
    single {
        Retrofit.Builder()
            .baseUrl("https://qiita.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    single {
        get<Retrofit>().create(QiitaService::class.java)
    }

    single {
        UserRepository(get())
    }

    viewModel {
        MainViewModel(get())
    }
}

MainActivity

ここまで定義できれば、あとは MainActivity で View を更新する処理を記述すれば完成です。
MainViewModel の LiveData<T>observe し、データ取得が完了したら View が更新されるようにします。

class MainActivity : AppCompatActivity() {
    private val viewModel : MainViewModel by viewModel()

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

        startKoin {
            androidLogger()
            androidContext(applicationContext)
            modules(appModule)
        }

        val binding : ActivityMainBinding = 
           DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = viewModel

        viewModel.user.observe(this, Observer {
            binding.userImageView.load(it.profile_image_url)
            binding.userNameValue.text = it.name
            binding.idValue.text = it.id
            binding.organizationValue.text = it.organization
            binding.descriptionValue.text = it.description
        })
    }
}
<?xml version="1.0" encoding="utf-8"?>

<layout>

    <data>

        <variable
            name="viewModel"
            type="kaleidot725.sample.ui.MainViewModel" />
    </data>

    <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=".ui.MainActivity">

        <ImageView
            android:id="@+id/user_image_view"
            android:layout_width="250dp"
            android:layout_height="250dp"
            android:layout_marginTop="32dp"
            android:background="@android:color/black"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_margin="32dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/user_image_view">

            <TextView
                android:id="@+id/user_name_title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_margin="8dp"
                android:text="User Name"
                android:textSize="16sp"
                app:layout_constraintEnd_toStartOf="@id/user_name_value"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                tools:text="User Name" />


            <TextView
                android:id="@+id/user_name_value"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_margin="8dp"
                android:textSize="16sp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toEndOf="@id/user_name_title"
                app:layout_constraintTop_toTopOf="parent"
                tools:text="Yusuke Katsuragawa" />

            <TextView
                android:id="@+id/id_title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_margin="8dp"
                android:text="ID"
                android:textSize="16sp"
                app:layout_constraintEnd_toStartOf="@id/id_value"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/user_name_title"
                tools:text="ID" />


            <TextView
                android:id="@+id/id_value"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_margin="8dp"
                android:textSize="16sp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toEndOf="@id/id_title"
                app:layout_constraintTop_toBottomOf="@id/user_name_value"
                tools:text="ID" />

            <TextView
                android:id="@+id/organization_title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_margin="8dp"
                android:text="Organization"
                android:textSize="16sp"
                app:layout_constraintEnd_toStartOf="@id/organization_value"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/id_title"
                tools:text="Organization" />


            <TextView
                android:id="@+id/organization_value"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_margin="8dp"
                android:textSize="16sp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toEndOf="@id/organization_title"
                app:layout_constraintTop_toBottomOf="@id/id_value"
                tools:text="Company Name" />

            <TextView
                android:id="@+id/description_value"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="32dp"
                android:textSize="18sp"
                app:layout_constraintTop_toBottomOf="@id/organization_title"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"
                tools:text="Description" />

        </androidx.constraintlayout.widget.ConstraintLayout>

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

おわりに

アプリケーションは Model Flow<T> を生成、 ViewModel で LiveData<T> を生成、そして View で LiveData<T> を observe してデータ更新という構成で実装しました。Room だと Room 側で Flow<T> に変換してくれるのですが、Retrofit では対応していなそうなので自身で Flow<T> に変換しなくてはならないみたいですね。

image.png

エラー処理を考慮するとまた印象が変わるかもしれませんが、LiveData<T>Flow<T> を組み合わせたパターンはとても実装しやすいですね。今後は LiveData<T>Flow<T> の組み合わせが主流になるんではないでしょうか。今回作成したサンプルは次に保存してありますので興味があれば閲覧お願いします。

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

【Android】Epoxyのモデル変更判定

はじめに

RecyclerViewを超絶簡単に実装できるEpoxyだけど、こいつを使い初めてから幾度となく値を変更しても見た目が変更されない不具合に悩まされ続けてきた。
その度になんとなく他の部分と同じように修正してどうにかしてきたけど、先日やっと根本的な原因がわかった。
もはや自分では忘れないと思うけど、何故か調べても同じようなことで悩んでいる人の記事とか出てこなかったので紹介する。

やりたいこと

選択したやつの色を変えてあげたい。

SNSのいいねボタンとか想像してもらえるとわかりやすいと思う。
押すたびにステータスが変わって、それに応じて見た目も変化して欲しい。

実装方法

特別な処理は入れていないけど、一応のっけとく。

◾︎SampleActivity

class SampleActivity : AppCompatActivity() {

    private lateinit var binding: ActivitySampleBinding
    private var epoxyController: SampleEpoxyController? = null
    private var sampleEpoxyModels: List<SampleEpoxyModel>? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_sample)
        setController()
    }

    private fun setController() {
        sampleEpoxyModels = (0..9).map {
            SampleEpoxyModel(it)
        }
        epoxyController = SampleEpoxyController(
            onClick = {
                val sampleEpoxyModel = sampleEpoxyModels?.find { sampleEpoxyModel ->
                    sampleEpoxyModel.id == it.id
                }
                sampleEpoxyModel?.isSelected = sampleEpoxyModel?.isSelected?.not() ?: false
                epoxyController?.setData(sampleEpoxyModels)
            }
        )
        binding.recyclerView.setController(epoxyController as EpoxyController)
        epoxyController?.setData(sampleEpoxyModels)
    }
}

◾︎SampleController

class SampleEpoxyController(
    private val onClick: (SampleEpoxyModel) -> Unit
) : TypedEpoxyController<List<SampleEpoxyModel>?>() {

    override fun buildModels(data: List<SampleEpoxyModel>?) {
        data?.forEach { sampleEpoxyModel ->
            itemSampleList {
                id("EpoxyModel_id:${sampleEpoxyModel.id}")
                sampleEpoxyModel(sampleEpoxyModel)
                onClick { _ ->
                    onClick.invoke(sampleEpoxyModel)
                }
            }
            itemDivider {
                id("divider")
            }
        }
    }
}

◾︎ activity_sample.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <com.airbnb.epoxy.EpoxyRecyclerView
                android:id="@+id/recycler_view"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:splitMotionEvents="false"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintBottom_toBottomOf="parent" />

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

◾︎ item_sample_list.xml

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

    <data>

        <variable
                name="sampleEpoxyModel"
                type="com.kl.testapp2.epoxymodelchange.SampleEpoxyModel" />

        <variable
                name="onClick"
                type="android.view.View.OnClickListener" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="80dp"
            android:background="@{sampleEpoxyModel.isSelected ? @color/colorPrimary : @color/white}"
            android:onClick="@{onClick}" />

</layout>

リストに設定したonClickのイベントを受け取って、Activityが保持しているListに対してfindして更新するアイテムを見つける。
取得したアイテムのフラグを反転させて再度setData()を実行している。

不具合内容

中の値を入れ替えて、しっかりsetDataしているのに見た目が変わらない。

sampleEpoxyModel?.isSelected = sampleEpoxyModel?.isSelected?.not() ?: false
epoxyController?.setData(sampleEpoxyModels)

更新後の値を確認してもやっぱりきちんと変わっている。
一応idを変えればViewHolderごと再生性できるんだけど、それをするとちらついて鬱陶しい。

原因&解決方法

色々試した結果、リストのフラグは変わっていても、インスタンス自体は同じなのが原因だということがわかった。
どうやらEpoxyは、保持しているリストの中の値が変わったかどうかは見ていないようで、参照しているインスタンスが変更されたかどうかで変更を検知してるみたい。
==equals()の違いだね。

ということは、変更する要素のindexに対して.copy()で新たに生成したインスタンスを入れてあげれば変更が検知されるはず。

onClick = {
    val index = sampleEpoxyModels?.indexOf(it) ?: -1
    sampleEpoxyModels?.set(
        index,
        it.copy(
            isSelected = it.isSelected.not()
        )
    )
    epoxyController?.setData(sampleEpoxyModels)
}

こんな感じで修正したら、きちんと動きました!!

まとめ

Epoxyでフラグ使ってViewの見た目を更新するときはインスタンスごと再生成してあげましょう!

おわり

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

Kotlin Coroutine 入門1: 起動と suspend

Kotlin 標準の並行プログラミング API である coroutine を理解1したのでまとめました。

本家のガイド は包括的で(上から読めば)丁寧に書いてあるのですが、実際に自分が読んだ際には理解に結構苦労したので、少し別のアプローチでの入門になります。

(はじめにお断り: Coroutine と Promise, async/await, Rx, etc…)

並行処理の API といえば、ここ数年だと「Promise」や「Rx」のような計算結果を表すオブジェクトを then()map() などでつなげていくスタイルか、「async/await」などと呼ばれるような計算結果オブジェクトを馴染みやすい形で書くスタイルが多いと思います。

coroutine の基本を学習する際には、一旦これらは忘れてもらった方が良いです。 なんとなく async/await っぽい位置づけと考えている方もいるかもしれません(筆者もそう思っていた一人です)。馴染みやすい書き方という意味では似ているのですが、背後の考えは結構違います。

Coroutine と suspend 関数

簡単な例

以下、簡単な使用例として通信処理を伴う処理を考えます。同期的に呼び出すとこうなると思います。

// メインスレッドで実行
fun runMain() {
    val data = Weathers.tomorrow()
    println("明日の天気: ${data}")
}

fun Weathers.tomorrow(): String {
    Thread.sleep(2000) // 長い処理を表すための仮実装
    return "晴れ"
}

▶️ 実行してみる

これを実行すると、2秒後に 明日の天気: 晴れ が出力されます。一方 coroutine を使うと以下のように書く事ができます2

// メインスレッドで実行
fun runMain(): Job = viewModelScope.launch {
    val data = Weathers.tomorrow()
    println("明日の天気: ${data}")
}

suspend fun Weathers.tomorrow(): String {
    delay(2000) // 長い処理を表すための仮実装
    return "晴れ"
}

▶️ 実行してみる

viewModelScope.launch { … } を呼び出す事でバックグランドで処理が開始されます。suspend, delay などの見慣れないキーワードが一部追加されていますが、主な記述内容は同期的な呼び出しと同じです。

最初の同期的な書き方では2秒間スレッドがブロックされてしまうという問題があります。この間ユーザーの操作がフリーズしてしまうのでアプリケーションでこのコードを書くのは現実的ではありません。一方 couroutine を使うと結果が出るまでの2秒間ユーザーは普通に操作ができます。便利!!??

処理の中断(suspend)

上のような差ができる理由は coroutine ではスレッドをブロックする代わりに処理を「中断」するからです。

最初の例では Thread.sleep() により2秒間メインスレッドを占有(ブロック)していて、その間他の処理がしたくてもメインスレッドが使えません。一方 coroutine の例で使った delay() 関数は2秒間ブロックするのではなく、メインスレッドを2秒間解放してからメインスレッドを再取得して処理を続行します。開放中は他の処理にスレッドを活用できます。これを「中断(suspend)」と呼びます。

フレームワークやライブラリの機能を使って同じような事を実現する事ためにはコールバック関数を用意する必要があるため、coroutine と比べると周りくどい書き方になりがちです。以下は Android による例です。

// メインスレッドで実行
fun runMain() {
    // 結果をコールバックを渡して呼び出し
    Weathers.tomorrow { data ->
        println("明日の天気: ${data}")
    }
}

fun Weathers.tomorrow(callback: (String) -> Unit): Unit {
    // 2秒後に処理を呼び出すようタイマーを設定する。
    // callback 処理はメインスレッドで行われるが、それまでの間スレッドは解放される
    val handler = Handler()
    handler.postDelayed({ callback("晴れ") }, 2000)
}

▶️ 実行してみる

delay() のように中断が起こる関数を「suspend 関数」と呼びます。通常の関数は中断しない(できない)ので、suspend 関数を呼び出す事ができません。 suspend 関数を呼びたい場合は launch の中で呼び出すか、関数に suspend キーワードを付けて suspend 関数にする必要があります。

CoroutineScope

coroutine を使った例では viewModelScope に対して launch { … } を呼び出す事で coroutine を起動していました。coroutine を起動する機能を持つオブジェクトは「CoroutineScope」と呼ばれます。

CoroutineScope は coroutine の起動だけではなく起動した coroutine を適切に終了する役割を担っています。例えば Android の KTX で提供されている3 viewModelScope は、画面を閉じた時に coroutine を自動でキャンセルしてくれます。 Android でユーザー操作による処理を実行する場合は viewModelScope から起動すると良いでしょう。

一方で Kotlin の標準で用意されている GlobalScope もあります。こちらは処理が自動でキャンセルされません。一般的には自動でキャンセルを行ってくれる scope を使った方が良いとされます4

coroutine を手動でキャンセルする事も可能です。 launch { … } メソッドの結果に対し cancel() を呼び出すとキャンセルする事ができます。

fun runMain(): Job = viewModelScope.launch {
    val data = Weathers.tomorrow()
    println("明日の天気: ${data}")
}

val job = runMain()
// 個別にジョブをキャンセルする。特に必要な場合のみ使う。
job.cancel()

runMain()
runMain()

// キャンセルすれば viewModelScope から起動したジョブを全てキャンセルできる。
viewModelScope.cancel() // しかし実際はフレームワーク側でやってくれるので不要

▶️ 実行してみる

Coroutine は難しくない

このように、coroutine の基本は「launch() で非同期処理を開始し、中断する関数には suspend をつける」というとてもシンプルなものです。並行プログラミングをする上で見落としがちなキャンセル処理も、CoroutineScope が適切にやってくれるようになっています。

他のプラットフォームにおける Promise のような概念もあるのですが、特に必要ない限りは suspend 関数で済ませるのが簡単で問題も少ないです。

とはいえ suspend 関数が充実していない段階では、suspend 関数だけで済ませるのは中々難しいと思います。Kotlin では従来の処理を suspend 関数にする方法をいくつか用意しているので、それを紹介します。

suspend 関数への変換

ブロック処理 → suspend 関数

一定期間待つような単純な処理であれば delay() という組み込みの suspend 関数を使えば実現できました。しかしそう言った suspend 関数がない場合は従来のブロックする処理から suspend 関数を作る必要があります。

一番最初の例にあったブロック版の関数を考えてみましょう。

fun Weathers.tomorrow(): String {
    Thread.sleep(2000) // 長い処理を表すための仮実装
    return "晴れ"
}

以下のように別のスレッドに切り替える事で、ブロック処理を suspend 関数に変換する事ができます。

suspend fun Weathers.tomorrow(): String = withContext(Dispatchers.IO) {
    Thread.sleep(2000) // 長い処理を表すための仮実装
    "晴れ"
}

withContext(Dispatchers.IO) { … } で別のスレッドを使って実行し、withContext() の処理が終わるまでの間メインスレッドを開放する事ができます。

標準では Dispatches.IO(IO処理用) の他に Dispatchers.Main(メインスレッド5)や Dispatchers.Default(計算用)があります。また、独自でスレッドプールを作る事もできます。

スレッドの切り替えについての詳細は、公式ドキュメントの Coroutine Context and Dispatchers に書かれてあります。

コールバック関数 → suspend 関数

すでに非同期処理用にコールバック形式の関数がある場合は、どうでしょう。先ほどの Handler を使った例を改善してみます。

fun Weathers.tomorrow(callback: (String) -> Unit): Unit {
    // 2秒後に処理を呼び出すようタイマーを設定する。
    // callback 処理はメインスレッドで行われるが、それまでの間スレッドは解放される
    val handler = Handler()
    handler.postDelayed({ callback("晴れ") }, 2000)
}

suspendCoroutine { … } を使う事で suspend 関数に変換する事ができます。

suspend fun Weathers.tomorrow(): String = suspendCoroutine { c ->
    // 2秒後に処理を呼び出すようタイマーを設定する。
    // callback 処理はメインスレッドで行われるが、それまでの間スレッドは解放される
    val handler = Handler()
    handler.postDelayed({ c.resume("晴れ") }, 2000)
}

ただし、suspendCancellableCoroutine を使った方が望ましいです。

suspend fun Weathers.tomorrow(): String = suspendCancellableCoroutine { c ->
    // キャンセル対応用コード1: コールバック処理を一旦変数で持つ
    val callback = Runnable {
        // キャンセル対応用コード2: キャンセルされている場合は何もしない
        if (c.isActive) c.resume("晴れ")
    }

    val handler = Handler()
    // キャンセル対応用コード3: coroutine のキャンセルが起きた時に Handler 側もキャンセルする
    c.invokeOnCancellation { handler.removeCallbacks(callback) }

    // 2秒後に処理を呼び出すようタイマーを設定する。
    // callback 処理はメインスレッドで行われるが、それまでの間スレッドは解放される
    handler.postDelayed(callback, 2000)
}

▶️ 実行してみる

ちょっと長くなってしまいましたが、これは主にキャンセル対応のためのコードがあるからです。coroutine は任意のタイミングでキャンセルする事ができるので、キャンセルされた時に元の非同期関数の処理もキャンセルする事が望ましいです。また、処理の要所要所でキャンセルされていないか(isActive が true か)チェックする必要もあります。

キャンセルと協調的マルチタスキング

「キャンセル対応」というのが出てきました。 suspend 関数へ変換する処理を適切に作るためには、coroutine のキャンセルの仕組みを理解した方が良いでしょう。

実は、CoroutineScope 等に対してキャンセルを呼び出してもいきなり実行中の処理が打ち切られるわけではありません。実際に打ち切られるポイントは限られており、またキャンセルに従わないで処理を続行する事も可能です。

キャンセルされた処理が実際に打ち切られるポイントは、主に「suspend 関数を呼んだタイミング」です。キャンセル対応された suspend 関数6はキャンセル対応しています)を呼び出した場合、CancellationException が呼ばれます。投げられた CancellationException をキャッチせずに coroutine を終了すれば、キャンセル処理が完了します。

そのため、suspend 関数が CancellationException を投げるかもしれない事を想定するだけでキャンセル対応できます。一方で、ブロック処理を suspend 関数を作るようなケースだと、isActiveyield でキャンセルが要求されているか確認したりするなどして、明示的にキャンセル対応しないと最後まで処理を続けてしまいます。

キャンセルについての詳細は、公式ドキュメントの Cancellation and Timeouts に書かれてあります。

まとめ

ここまで Kotlin の coroutine の基本、特に起動と中断について解説しました。「時間のかかる処理をスレッドをブロックせずに非同期で行いたい」といった用途であれば、今回の内容で大体事足りるのではないかと思います。

次回は並列処理を行う際の話や、Kotlin の coroutine で特徴的な structured concurrency について解説したいと思います。


  1. 入門読んで Hello, world! ができるようになった程度という意味。Flow も分かってないし…。 

  2. Android と KTX を使った場合の書き方。 

  3. Android の場合は viewModelScope の他に lifecycleScope が用意されています(2020/03月時点)。 

  4. Kotlinの標準ライブラリ開発のリーダーである Roman Elizarov さんによるThe reason to avoid GlobalScope に GlobalScope を避けるべき理由が書かれてあります。 

  5. Dispatchers.Main の実装はプラットフォーム側が用意するもので、素の Kotlin では使用できません。 

  6. 標準の suspend 関数(delay()など)は全てキャンセル対応しています。 

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

FlutterのWebView内からメールアプリを起動してみた件

はじめに

 みなさんはFlutterでWebViewを使用したことありますか?
 すみません、そんなことはどうでもいいです。今回はタイトルにもある通り、FlutterのWebView内から端末内のメールアプリを起動してみます。

最終結果

 最終的に、下の画像みたいにメールアプリを起動する画面を出します。

66005.jpg

いつメールアプリを起動するのか

 普通にWebViewを出してるだけだと、メールアプリを起動する場面はなかなかありませんが、あるとするなら、「お問合せはコチラ」みたいなとこクリックしてメールアプリを起動するみたいな感じです!
 掲示板みたいに、自分の名前とかメアドを入力してお問合せできるページが用意されていれば、特に問題はないんですけど、全部がそういうわけじゃないからな~~~

普通にWebViewからメールアプリを起動してみる

 普通にWebViewからメールアプリを起動してみます。Flutterにはなぜか最初からWebViewは入っていないため、ライブラリを用います。今回使用するライブラリは「webview_flutter」です。
 また、メールアプリを起動するものには、urlに「"mailto:メアド"」がはいってます。(例:mailto:example@ex.com

 今回開くURLは私が鹿児島大学で所属している「KADAI INFO」のサークル紹介ページへアクセスします。(よかったらKADAI INFO見てほしい!)

main.dart
WebView(
    initialUrl:"https://kadai-info.com/2020/03/13/kabadiclub-2020/", // URL読み込み
    javascriptMode: JavascriptMode.unrestricted,  // javascriptの有効化
);

 これを開いてみると以下のような結果になります。
66004.jpg

ここは、普通のページなので、特に問題なく表示されます。しかし、この「入部希望の連絡をする」をタップしてみると以下のようになります。
66006.jpg

はい、なんやらエラーページがでました。urlを見てみると、「mailto:〇〇@gmail.com」とメールアプリを起動するURLが設定されています。
パソコンには既にこういうアドレスを開く機能が備わっていますが、アプリでは自分で設定しないとこのようなエラーがでます。

url_launcherを使う

 こういったメールアプリを起動するような特殊なURL処理をするライブラリとして「url_launcher」というものがあります。
 しかし、こちらのライブラリの例題は、WebView上で機能するものではなく、自分でボタンやら設定して、それがタップされたら起動する~みたいなやつです。
 使い方はめちゃくちゃ簡単で、WebViewとほぼ同じ感じです(下のやつを参考にしてくれい!)。

hoge.dart
  const url = "指定のURL";

  // 有効なアドレスだったら
  if (await canLaunch(url)) {
    await launch(url);  // メールアプリを開く

  // 有効なアドレスでないなら
  } else {
    throw 'Could not launch $url';
  }

 しかし、これを使用するためにはURLを知っておく必要があります。1つしかないなら直接打ち込めばいいですが、このサークルページは他にもいくつもあり、全部あらかじめ用意しておくのはマジきつい。
 そこで、WebViewからURLを取得します。

WebViewからURLを取得する

 そもそもWebViewからどうやって、URLを取得するのか?
 webview_flutterの中の「NavigationDeligate」から取得できます。これは、URL先に遷移するか制御するものです。また、これを使用するときは引数に「NavigationRequest」を渡します。つまり、こいつは遷移先のURLをどこかに保持している可能性があります。ライブラリを見てみると案の定「String url」を持っていたので、こいつにアクセスしてURLを取得します。

hoge.dart
navigationDelegate: (NavigationRequest request) {
    String url = request.url; // request.urlで取得できる(requestの名前は何でもいい)
}

これで、URLは取得できましたので、あとはこのURLとurl_launcherを使用するだけでOKです!

メールアプリを起動させてみる

 では、最後に実際に起動させてみます。コードは以下

main.dart
_launchURL(String url) async {
    // 有効なアドレスだったら
    if(await canLaunch(url)){
      await launch(url);  // メールアプリを起動
      return true;
    }
    // 有効なアドレスでないなら
    else{
      return false;
    }
  }

return WebView(
    initialUrl: url,
    navigationDelegate: (NavigationRequest request) {
        // メールアプリを起動する必要があったら
        if(_launchURL(request.url)){
           return NavigationDecision.prevent;  // 前のページに戻る
        }
        // メールアプリを起動する必要がなかったら
        else{
           return NavigationDecision.navigate;  // そのままページを読み込む
        }
    },
);

メールアプリを起動したら前のページに戻る処理をしていますが、これはエラーページを表示させないためです。
これしないと↓みたいになって気持ち悪い。
66007.jpg

だから、メールアプリを起動する必要があるようなページだった場合は、前のページに戻って、そうでないならそのまま突き進む処理を施したってわけだぜ!
まあ、最初にも載せてるけど、戻れば↓みたいに遷移前の画面でメールアプリ起動の画面を出せる

66005.jpg

終わりに

 今回はメールアプリを起動させてみました。ていうか、メールアプリって最初から言ってるけど正しい言い方は分からんから別に指摘しないでくれ!頼む!フリじゃない!
 また、自分で調べて日本語記事がないやつあったら書いていきます!ではまた!

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

61st聖光祭 アプリ局 AndroidJavaの教科書

1. AndroidStudioの操作一覧

英語さえ読めれば大丈夫だと思うが、一応まとめとく。

1.1 プロジェクトのつくり方

  1. ホーム画面に行く。
  2. Start a new Android Studio projectをクリック
  3. Empty Activityを選択
  4. Nextをクリック
  5. Nameを入力 (英語 全単語頭文字大文字 その他小文字 記号特殊文字禁止)
  6. Package Nameを入力 (英語 単語間は. 全文字小文字 特殊文字禁止)
  7. LanguageJava固定
  8. Minimum API levelはお好みで。(23推奨)
  9. Finishをクリック
  10. ビルドを待つ

1.2 画面の見方

ファイルツリー

左にはファイルツリーが表示される。-ボタンで非表示に。非表示後はさらに左の1:Projectをクリックで表示できる。

デバッグ関係

下にはデバッグやビルドをしたときのログが表示される。実行時は勝手に表示されるので作業中は必要ないと思われ。ログ以外にも補助ソフトをいれるとCheckStyleFind-Bugも表示されるが、これらの切り替えはさらに下のタブでできる。TODO機能はぜひ使ってほしい。

プロセスステータスバー

デバッグ関係の下の下。右側には左から行番号:左からの文字数、文字コード、インデントのスペース数、ロック、フィードバック二つ、なんかが表示されている。これらは使わないでくれ(切実)。ロックは押すと編集不可になり、文字を入力するとダイヤログがでる。

問題なのは通常では何もないがバーのまんなかと右側である。ビルドすると真ん中には現在何を行っているか表示され、右側にはプロセスバーが現れる。これがあるときはファイルの編集しない方がいい。

ツールバー(OS標準)

ウィンドウの一番うえの左からFileEdit...と続いているツールバー。ほとんどのソフトに存在するOSが描画した(プログラミング脳)ツールバーである。かなり使うのでツールバーといわれたらここを見てほしい。

ショートカットツールバー(AS装備)

ツールバーの下にあるAndroidStudio固有のツールバー。左にいま開いてるファイルの階層が表示される。階層をまたぐ移動はのちほど紹介するファイルタブではなくここからやるといい。右にはユーザーが編集できるショートカットがある。は実行、虫みたいなマークはデバックである。ここで注意なのがデバックを使うことである。実行はエラーの詳細、現状のログを出してくれない。実行はアプリの動画を撮ったりしたり完成品で遊ぶときに使用しよう。他にも上級者にはありがたい機能のショートカットがあるが、割愛する。

メイン画面

メインの作業画面。上部にはファイルタブがあり、複数の(開かれている)ファイルを移動できる。xmlのときはそれ用の画面になる。他にも画像を開くと編集画面になったり、音声や動画を開くと再生バーがでてきたりする。

Palette-パレット(xml)

xmlを開くとメイン画面の左上に表示されるView一覧。ここから使うViewをとってくる。

Component Tree-コンポネントツリー(xml)

xmlを開くとメイン画面の左下に表示される配置済みのView一覧。PaletteからViewをここにドラッグしよう。あとは階層と上下関係を意識してレッツ!ドラッグアンドドロップ!(ガヴリ―ルドロップアウトみたいだよね←どこが)

メイン画面(xml)

Android画面が表示されてるところ。ここいじることは少ない。ちなみに右に表示されてる青い画面は領域の境とか大きさとか書かれているので参考にするといい。あとは、Android画面の左下のなぞの//はドラッグすると画面サイズを変えられる。(結構これすごい)

また、上部にはたくさんの項目があるが、端末の向きを変えたり画面を本当に存在する端末(Googleが監修した端末のみ)のプリセットを利用できる。(Nexus5!!!)ほかはあんまりいじんないほうがいいよ(60回の聖光祭でやらかしました)

Attributes-アトリビュート(xml)

Viewの属性(パラメータ)を変更する画面。右側に縦長にあるやーつ。編集したいViewを選択してから変えようね?パラメータはViewによって違うし、多すぎるので覚える必要はない。英語よめ。

1.3 Androidのディレクトリ事情

Pathはファイルツリーでのパス。JavaはJava内での表記方法。xmlはxml内での表記方法。
ちなみにファイル名拡張子を含みません。

Javaファイル

Path:app/java/パッケージ名/Jファイル名
Java:パッケージ名.Class名
xml :パッケージ名.Class名

画面を設計するxmlファイル

Path:app/res/layout/ファイル名
Java:R.layout.xmlファイル名
xml :なし

素材の画像

Path:app/res/drawable/ファイル名
Java:R.drawable.ファイル名
xml :@drawable/ファイル名

その他の素材(動画とか音声とか)

Path:app/res/row/ファイル名
Java:R.row.ファイル名
xml :@row/ファイル名
rowフォルダは新しく作る。

その他のxml(string.xmlとか)

Path:app/res/values/ファイル名
Java:R.ファイル名.
xml :@ファイル名/

ちょっとした例

string.xml
<resources>
    <string name="hayaku_tugi_coi">Auribus oculi fideliores sunt.</string>
</resources>

このとき、@string/hayaku_tugi_coiとなる。

1.4 Github関連(Githubにつながっている場合)

リモートレポジトリを作らないとGithubは利用できないので、新規のプロジェクトには使えません。

Commit

こまめに結果にコミット!
1. ツールバーのVCSをクリック
2. Commitをクリック

また、ショートカットのチェックマークでもできます。

Push

  1. ツールバーのVCSをクリック
  2. Gitにホバーする
  3. Pushをクリック
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AsyncTaskをTHREAD_POOL_EXECUTORで実行した際のPoolサイズ

AsyncTaskをexecuteOnExecutor(THREAD_POOL_EXECUTOR)で実行した際のPoolサイズについて調べました。

結果

THREAD_POOL_EXECUTORのPoolサイズはCPUの数で決まり、PoolサイズはMath.max(2, Math.min(CPU_COUNT - 1, 4))となります。

詳細

THREAD_POOL_EXECUTORは、AsyncTaskで生成されたThreadPoolExecutorで、生成時のパラメータは下記の通りです。

CcorePoolSize:Math.max(2, Math.min(CPU_COUNT - 1, 4))
maximumPoolSize:CPU_COUNT * 2 + 1
※ workQueuのサイズは128

AsyncTask.java
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    // We want at least 2 threads and at most 4 threads in the core pool,
    // preferring to have 1 less than the CPU count to avoid saturating
    // the CPU with background work
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final int KEEP_ALIVE_SECONDS = 30;

    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>(128);

    public static final Executor THREAD_POOL_EXECUTOR;
    static {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
                sPoolWorkQueue, sThreadFactory);
        threadPoolExecutor.allowCoreThreadTimeOut(true);
        THREAD_POOL_EXECUTOR = threadPoolExecutor;
    }

ThreadPoolExecutorのmaximumPoolSize

ThreadPoolExecutorのPoolサイズは、corePoolSizeおよびmaximumPoolSizeによって設定された境界に従って、自動的に調整されます。
corePoolSizeを超えているが、maximumPoolSizeスレッドが実行されていない場合、キューがいっぱいの場合にのみ新しいスレッドが作成されます。
※ THREAD_POOL_EXECUTORのworkQueuのサイズは128

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

AndroidとmacOSでファイル共有する原始的な方法

macOS使っているのに、iPhoneよりもAndroidが好きというエンジニアは多いと思います。AndroidならTermuxやいろいろ使えます。

それで、AndroidとmacOSで最も一番簡単にファイル共有する方法が分かったのでメモしておきます。それは、FTPやSSHでファイルのやりとりをするんです。なーんだという感じですが、広告がいっぱいのファイル共有アプリなんか使うより確実で、SSH使えば安全です。

Ftp Serverを使う

簡単な広告が出ますが、Android上でFTPサーバーを起動する一番簡単な方法です。

Android上でこれでオンにして、macOSのFilezilla ClientなどのFTPクライアントでアクセスします。開発者であれば最初からインストールされていることでしょう。

なお、同作者がSSH Serverも公開しているのですが、手元のAndroidでは接続できませんでした。

TermuxでSSH/SFTPを使う

少し前に、AndroidのターミナルエミュレーターTermuxについて書きました。Termuxを設定する際、MacからSSHにも接続するように設定しました。sshdをインストールして、Mac側で公開鍵を作り、Termuxに登録します。すると、上記で紹介したFilezillaを利用して、Androidにファイルが転送できます。

改めて、手順を整理すると以下のようになります。

  • (1) ストアでTermuxをインストール
  • (2) Termuxを起動して、termux-setup-storageを実行
  • (3) 必要なアプリをインストール pkg update && pkg install git vim wget tmux
  • (4) sshdをインストール pkg install sshd
  • (5) Macでターミナルを起動して公開鍵を作る ssh-keygen -t rsa
  • (6) Macのid_rsa.pubの内容をLINEやメールでAndroidに送り、~/.ssh/authorized_keysに追記する(なければ作る)。

以上で、設定が完了です。以下の手順で、Filezillaを使ってファイルの転送ができます。

  • (1) sshdを起動 sshd
  • (2) TermuxでAndroidのIPアドレスを調べる ifconfig | grep 192
  • (3) Mac側でsshdに接続してみる ssh hoge@192.168.xxx.xxx -p8022(xxxには上記のIPアドレス)
  • (4) 正しく接続できたなら設定完了。FilezillaでAndroidに接続すると、安全にファイルの送受信が可能。

二回目以降は設定が要らないので、そこそこ気軽に使えます。

その他の方法

コメント欄で教えていただいた方法ですが、Android StudioのDevice File Managerを使ったファイル送受信も良いですね!

他にも、開発者に向けた手軽な方法があれば、コメント欄で教えてください!

まとめ

手軽なのは、Ftp Serverを使ってファイルを転送することですが、平文でファイルを転送しないといけないので、ちょっと心配です。そこで、Termuxでsshdを起動して、SFTPでファイル転送すれば安心して転送できます。ただし、Termuxでコマンドを打つのはそこそこ面倒なので、使い分けると良いでしょう。

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

kotlinでフェードイン、フェードアウトを実装した話

qiitaが「いいね」からLGTM(Look Good To Me)に変わりましたね。

くだらない上にただの雑談が数百いいねついてる現状を見てたらそりゃもう運営ナイスです
くだらん雑談で技術なしでバズらせたいならツイッターやってろ三流がって話。

今回はフェードイン、フェードアウトを実装しました。

できることとしては
チュートリアルなんかでよくある、ゆったりふわーと真っ白い画面からチュートリアルにうつって、ゆったりふわーっと真っ白い画面から元いた画面に戻るあれです(伝われ)

Animationってライブラリがあって数秒かけて透過させる的な処理はできるのですが、連打したら透過させる処理を繰り返してしまったりそのままでは使い物にならなかったので。

①fade outを時間を指定してできる(3秒経過後に関数を実行したかったができなかったからその機能を追加した。例えばfade out後にdialogFragmentをdismiss()したいなど)
②fade in, fade out中に他のanimationの処理が割り込まないようにisAnimagingというフラグを用いた(連打時のバグ防止)

ではまずはdialogFragmentを表示するactivityから

MainActivity.kt
class MainActivity : AppCompatActivity(), BaseFullScreenDialogFragment.Callback {

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

        button.setOnClickListener {
            showFullScreenDialog()
        }
    }

    override fun onClickScreen() {
        comeBack()
    }

    private fun showFullScreenDialog() {
        // 付け足したければ
        val bundle = Bundle()
        val dialog = FullScreenDialogFragment.newInstance(bundle)
        dialog.setOnCallBack(this)
        dialog.show(supportFragmentManager, "dialog")
    }
    private fun comeBack() {
     // ダイアログを閉じたあとはここを通ります
        val a = 334
    }
}

buttonってのはfade inのトリガーになるボタンです。

dialogでフルスクリーンのチュートリアルを表示しています。
setOnCallBackでは画面のタップを受けたらdismiss()するっていう処理を書いています。

で、FullScreenDialogFragmentについてのソースコードを

FullScreenDialogFragment
class FullScreenDialogFragment : BaseFullScreenDialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val dialog = super.onCreateDialog(savedInstanceState)
        dialog.setContentView(R.layout.dialog_full_screen)
        dialog.background_img.setOnClickListener {
            dialog.background_img.fadeOut(3000) {
                super.callback!!.onClickScreen()
                this.dismiss()
            }
        }
        return dialog
    }

    // showの後に呼ばれる
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        dialog.background_img.fadeIn(3000)
        return super.onCreateView(inflater, container, savedInstanceState)
    }


    companion object {
        private var dialogFragment: FullScreenDialogFragment? = null

        fun newInstance(bundle: Bundle): FullScreenDialogFragment {
            if (dialogFragment == null) {
                dialogFragment = FullScreenDialogFragment()
            }
            dialogFragment!!.arguments = bundle
            dialogFragment!!.isCancelable = false
            return dialogFragment!!
        }
    }
}

onCreateView()のオーバーライドにてfadeIn()をやっています。
これはdialog().show()のあとに呼ばれるメソッドです。

このdialog fragmentが継承してるクラスはBaseFullScreenDialogFragmentです。↓

BaseFullScreenDialogFragment
abstract class BaseFullScreenDialogFragment : DialogFragment() {

    var callback: Callback? = null
    private val ARG_LISTENER_TYPE = "listenerType"

    // リスナーのタイプを保持するためのenum
    private enum class ListenerType {
        ACTIVITY, FRAGMENT, OTHER
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val dialog = super.onCreateDialog(savedInstanceState)

        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)

        return dialog
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        val listenerType = arguments!!.getSerializable(ARG_LISTENER_TYPE) as ListenerType?
        if (listenerType == ListenerType.ACTIVITY) {
            this.callback = activity as Callback
        } else if (listenerType == ListenerType.FRAGMENT) {
            callback = targetFragment as Callback?
        }
    }

    override fun onStart() {
        super.onStart()
        // ダイアログを全画面にする
        dialog?.window?.apply {
            setFlags(
                FLAG_FULLSCREEN,
                FLAG_LAYOUT_IN_SCREEN
            )
            setBackgroundDrawable(ColorDrawable(Color.WHITE))
            setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
        }
    }

    interface Callback {
        fun onClickScreen()
    }

    fun setOnCallBack(_cbj: Callback) {
        callback = _cbj
        // リスナーのタイプを保持
        val listenerType: ListenerType?
        if (this.callback == null) {
            listenerType = null
            setTargetFragment(null, 0)
        } else if (this.callback is Activity) {
            listenerType = ListenerType.ACTIVITY
            setTargetFragment(null, 0)
        } else if (this.callback is Fragment) {
            listenerType = ListenerType.FRAGMENT
            setTargetFragment(this.callback as Fragment?, 0)
        } else { //その他の場合
            listenerType = ListenerType.OTHER
            setTargetFragment(null, 0)
        }

        arguments!!.putSerializable(ARG_LISTENER_TYPE, listenerType)
    }
}

画面回転するとdialogを呼び元との接続がきれてしまうので呼び出し元を保持する、という方針です。
画面のタップを検知するためにcallbackを設けました。(このタップを検知してfade in, fade outの処理をします)
また、onStart()にて全画面表示にしています。

ちなみにxmlは

dialog_full_screen.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <com.free.myapplication.extension.AnimationImageView
        android:id="@+id/background_img"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/miri" />

</FrameLayout>

とまあこんな感じでやれば実装できます

githubは
https://github.com/takuma0223/tutorial

追記: 画面回転でメンバが消えると指摘があったので直しました(家帰ったら点検します)

追記2: 家で確認したところ、バックグラウンドにいったら落ちました。
serializableをCallbackインターフェースでimplementして無理やりbundleに保持したのですが、それがなにやらまずかったようで、調べたら参考になる記事がありました(方針は全く違う)
https://qiita.com/KazaKago/items/999ac7f7392de4657f30

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