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

Coroutines私的メモ1

これは私的なCoroutinesに関するメモです。
今回はlaunch, runBlocking, join, supervisorScopeについて触れます。
もし指摘点ありましたら遠慮なくまさかり飛ばしてもらえればと思います。

Coroutinesとは

中断(suspend)と再開(resume)の機能によってスレッドよりも低コスト・可読性高く非同期処理を扱える

導入

Kotlin1.3でstableになりました。

dependencies {
  def coroutines_version = "1.3.2"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
}

stableはこちらで確認できます

前提

現在のスレッドがわかるようにloggerメソッドを仕込むことにします

fun logger(msg: String) = println("${Thread.currentThread().name} : $msg")

GlobalScope.launch

GlobalScopeはコルーチンのスコープで、すべてのコルーチンはいずれかのスコープに紐づくことになっています。

fun normalLaunch() {
    logger("start")
    GlobalScope.launch {
        logger("Hello")
    }
    logger("World")
}

試しにこのようなコードを書いた場合は

main : start
main : World

になります。launchはその実行中のスレッド(今回はmain)をブロックしないためです。

そのため処理したいならほんの少しだけスレッドを停止させてあげれば可能です。

fun normalLaunch() {
    logger("start")
    GlobalScope.launch {
        logger("Hello")
    }
    Thread.sleep(100L)
    logger("World")
}

実行結果はこのように

main : start
DefaultDispatcher-worker-1 : Hello
main : World

ただしアプリ開発やサーバサイド開発するうえで、このようなコードをずっと書いていくわけにもいかないと思います。

runBlocking

そこで、runBlockingを使います。このブロックで囲まれた部分は現在のスレッドをブロックして実行します。

fun normalRunBlocking() {
    logger("start")
    runBlocking {
        logger("Hello")
    }
    logger("World")
}

結果は

main : start
main : Hello
main : World

runBlockingのブロックが終了することを待ってから最後のloggerへ移ります。

では、コルーチン内で他のコルーチンを記述する場合はどうなるのでしょうか。(=親子関係)

Job#Join

子のGlobalScope.launchで記述されたJobに対してjoin()することで親のコルーチンは子のコルーチンの終了を待機します。
変数宣言せずそのまま記述してもよいのですがこちらのほうが管理がずっと楽になりますね。

fun parentChild() {
    val job = GlobalScope.launch {
        delay(1000L)
        logger("Hello")
    }
    logger("start")
    runBlocking {
        job.join()
        logger("World")
    }
    logger("end")
}

結果としては

main : start
DefaultDispatcher-worker-1 : Hello
main : World
main : end

Child coroutinesの扱い

このようなコードを仕込んでみます
2つの子関係にあるコルーチンが存在しています

fun parentChildWhenException() {
    suspend fun child1() {
        delay(2000L)
        logger("child1")
    }
    suspend fun child2() {
        logger("child2")
        delay(1000L)
        throw RuntimeException("Exception!")
    }
    try {
        logger("start")
        runBlocking {
            launch { child1() }
            launch { child2() }
        }
        logger("end")
    } catch (e: Exception) {
        logger("${e.message}")
    } finally {
        logger("finally")
    }
}

実行すると以下になります

main : start
main : child2
main : Exception!
main : finally

子関係にあるchild2()側で1秒後に例外が投げられることで、child1()は実行されず親のコルーチンがキャンセルされます。
ではchildに対してExceptionをそれぞれ管理しつつきちんと実行されるにはどうしたらよいでしょう。

supervisorScope

childに関係なく実行させたい場合はsupervisorScopeを使います。試しに上記のコードにそれだけを挿入したコードを実行してみます。

fun supervisorScope() {
    suspend fun child1() {
        delay(2000L)
        logger("child1")
    }

    suspend fun child2() {
        logger("child2")
        delay(1000L)
        throw RuntimeException("Exception!")
    }
    try {
        logger("start")
        runBlocking {
            supervisorScope {
                launch { child1() }
                launch { child2() }
            }
        }
        logger("end")
    } catch (e: Exception) {
        logger("${e.message}")
    } finally {
        logger("finally")
    }
}

結果としては

main : start
main : child2
Exception in thread "main" java.lang.RuntimeException: Exception!
    at com.example.todoappsandbox.CoroutinesKt$supervisorScope$2.invokeSuspend(Coroutines.kt:76)
    at .. (略)
main : child1
main : end
main : finally

のようになり、child1自体も実行されるようになりました。一方catchされずExceptionが挙げられてしまいました。supervisorを使う場合は子の例外が親に伝播されないためです。そのため、子自身に例外処理を定義する必要があります。

CoroutineExceptionHandler

CoroutineExceptionHandlerを使って例外を定義できます。
それを対象の子のコルーチンに挿入して対応ができます。(launch(handler))

fun supervisorScopeHandleChildException() {
    suspend fun child1() {
        delay(2000L)
        logger("child1")
    }

    suspend fun child2() {
        logger("child2")
        delay(1000L)
        throw RuntimeException("Exception!")
    }

    val handler = CoroutineExceptionHandler {_, e ->
        logger("Handle Child Exception: ${e.message}")
    }
    try {
        logger("start")
        runBlocking {
            supervisorScope {
                launch { child1() }
                launch(handler) { child2() }
            }
        }
        logger("end")
    } catch (e: Exception) {
        logger("${e.message}")
    } finally {
        logger("finally")
    }
}

これで例外なく全ての子関係のコルーチンが実行できるようになりました。

main : start
main : child2
main : Handle Child Exception: Exception!
main : child1
main : end
main : finally

次回はdispatchers, withContext, async/awaitあたりを触れていきたいです

参考

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

Kotlin+Realm+RecyclerViewを使ってTodoアプリを作る

Realm を使ったシンプルなサンプルアプリを作ってみました。
最初にダミーデータを作成しRecyclerViewを使ってリスト表示します。
今回はセルがタップされたときにデータを削除し、メッセージをToastで表示するようにしました。

全てのソースコードはこちらにあります。(GitHub)

環境

  • Kotlin 1.3.50
  • Realm 5.15.2
  • realm-android-adapter 3.1.0
  • recyclerview-v7:29.1.0
  • cardview-v7:29.1.0
  • targetSDK: 29

手順

  1. Realmの初期設定
  2. CustomAdapterの作成
  3. データ作成と一覧表示
  4. Itemをクリックしたときの処理を追加する

1. Realm の初期設定

Realm 公式ドキュメントに乗っ取りながらやると良いと思います。
https://realm.io/docs/java/5.15.2/

build.gradle の dependencies に以下を追加

classpath "io.realm:realm-gradle-plugin:5.15.2"

app/build.gradle の上部に以下を追加

apply plugin: 'kotlin-kapt'
apply plugin: "realm-android"

app/build.gradleのdependenciesに以下を追加

implementation 'com.android.support:recyclerview-v7:29.1.0'
implementation 'com.android.support:cardview-v7:29.1.0'
implementation 'io.realm:android-adapters:3.1.0'

[補足] realm-android-adapters について

Realm公式が出しているAdapterのライブラリです。表示しているデータの中身が変わったときにViewを更新するためnotifyDataSetChanged()等を呼ぶことが多いですが、Realmを使う場合DBの内容が書き変わったことを受け取りView を更新しなければならず、処理が複雑になることが考えられます。

このライブラリを使うと、RealmのDB内が書き換わったときに自動でViewを更新してくれて便利なので、今回はこれを使用します。
(ちなみに、ListViewとRecyclerViewの2つに対応しています)

参考: realm/realm-android-adapter

Applicationクラスの作成

Applicationクラスを作成します。アプリを起動したときに実行するようにし、Realmのinitializeの処理を行います。
今回はアプリ名がRealmTodoなのでRealmTodoApplication.ktという名前で作成しました。

RealmTodoApplication.kt
class RealmTodoApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        Realm.init(this)
        val realmConfig = RealmConfiguration.Builder()
            .deleteRealmIfMigrationNeeded()
            .build()
        Realm.setDefaultConfiguration(realmConfig)
    }
}

※ すでにDBにデータがある状態でモデルとなるクラスを変更(例えばプロパティを追加など)した場合、アプリを削除し再インストールする必要が出てきて正直手間になります。そこで、RealmConfigurationにて.deleteRealmIfMigrationNeeded()を設定すると、もともとあったデータを全て削除してくれるようになりこの手間を省くことができるようになります。

参考: Migrations | Realm Java 5.15.2

また、AndroidManifest.xmlのタグにname属性を追加し、先ほど作成したApplicationクラスを指定します。

AndroidManifest.xml
    <application
        android:name=".RealmTodoApplication"
        /* 省略 */
    ></application>

</manifest>

※ここでApplicationクラスを確認するとクラス名が灰色になっており参照できていないように見えますが、実際はちゃんと参照できているのでこのままでOKです。

これでRealm関連の初期設定は完了です。

2. CustomAdapterの作成

リストのItemとなるレイアウトの作成

CardViewを使っていい感じのリストにします。

list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    card_view:cardElevation="2dp">

    <LinearLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:background="?android:attr/selectableItemBackground"
        android:clickable="true"
        android:focusable="true">

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_margin="8dp"
            card_view:srcCompat="@mipmap/ic_launcher" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            android:orientation="vertical">

            <TextView
                android:id="@+id/contentTextView"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="2"
                android:gravity="center_vertical"
                android:text="やること"
                android:textSize="18sp" />

            <TextView
                android:id="@+id/dateTextView"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:gravity="center_vertical|end"
                android:text="2019/11/20 12:00:00" />

        </LinearLayout>

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

モデルの作成

RealmObjectを継承したモデルを作成します。@PrimaryKeyというアノテーションをつけると一意なプロパティとして定義することができ、その値をUUID.randomUUID().toString()とすると一意なIDを生成してくれます。(他にもAutoIncrementする方法もあります。)

参考: Realmで一意なPrimaryKeyを設定する方法二つ

Task.kt
open class Task(
    @PrimaryKey open var id: String = UUID.randomUUID().toString(),
    open var imageId: Int = 0,
    open var content: String = "",
    open var createdAt: Date = Date(System.currentTimeMillis())
) : RealmObject()

CustomAdapterの作成

ここでRealmRecyclerViewAdapterを継承したCustomAdapterを作成します。
タスクのリストを表示するのでTaskAdapter.ktとしました。

TaskAdapter.kt
class TaskAdapter(
    private val context: Context,
    private var taskList: OrderedRealmCollection<Task>?,
    private val autoUpdate: Boolean
) :
    RealmRecyclerViewAdapter<Task, TaskAdapter.TaskViewHolder>(taskList, autoUpdate) {

    override fun getItemCount(): Int = taskList?.size ?: 0

    override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
        val task: Task = taskList?.get(position) ?: return

        holder.imageView.setImageResource(task.imageId)
        holder.contentTextView.text = task.content
        holder.dateTextView.text =
            SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.JAPANESE).format(task.createdAt)

    }

    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): TaskViewHolder {
        val v = LayoutInflater.from(context).inflate(R.layout.list_item, viewGroup, false)
        return TaskViewHolder(v)
    }

    class TaskViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val imageView: ImageView = view.imageView
        val contentTextView: TextView = view.contentTextView
        val dateTextView: TextView = view.dateTextView
    }

}

autoUpdateというパラメータをtrueにして渡すと、DBを更新した際にRecyclerViewを自動で更新してくれるようになります。
またSimpleDateFormatを使うと、Date型を指定フォーマットでStringに変換できます。
参考: SimpleDateFormat | Android Developers

これでCustomAdapter周りも完了です。

3. データ作成と一覧表示

次に、Realmを用いたDBへの書き込みと読み込み、リストへの一覧表示を実装します。今回は一旦ダミーデータを10件作成し、そのリストを表示するような処理にしました。

MainActivity.kt
class MainActivity : AppCompatActivity() {

    private val realm: Realm by lazy {
        Realm.getDefaultInstance()
    }

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

        val taskList = readAll()

        // タスクリストが空だったときにダミーデータを生成する
        if (taskList.isEmpty()) {
            createDummyData()
        }

        val adapter = TaskAdapter(this, taskList, true)

        recyclerView.setHasFixedSize(true)
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = adapter

    }

    override fun onDestroy() {
        super.onDestroy()
        realm.close()
    }

    fun createDummyData() {
        for (i in 0..10) {
            create(R.drawable.ic_launcher_background, "やること $i")
        }
    }

    fun create(imageId: Int, content: String) {
        realm.executeTransaction {
            val task = it.createObject(Task::class.java, UUID.randomUUID().toString())
            task.imageId = imageId
            task.content = content
        }
    }

    fun readAll(): RealmResults<Task> {
        return realm.where(Task::class.java).findAll().sort("createdAt", Sort.ASCENDING)
    }

}

realmの変数ですが不変なオブジェクトとして扱いたかった(valで定義したかった)ので、lazyという委譲プロパティを使って遅延初期化をしています。これはこの変数が最初に参照されたときに{}内の値をsetして初期化します(2回目以降の参照では最初にsetした値を参照します)。
参考: 委譲プロパティ - Kotlin Programming Language

readAll()の中で.sort("createdAt", Sort.ASCENDING)をして作成したときの時刻を基準に昇順で取得しています。これは要素の削除をしたときに、リストの順番がバラバラになってしまう仕様を回避するためにこの実装にしました。
参考: Realm: Order of records was changed - Stack Overflow

また、create() 内のcreateObject()ですが、1つ目の引数にモデル、2つ目の引数に@PrimaryKeyで設定したプロパティの値を代入して生成します。(これがないと落ちます)
createObject()以外にもcopyToRealm()insert()などがあります。
参考: Creating objects | Realm Java 5.15.2

実行すると以下のようになります。(DB内に保存しているので、アプリを終了し再起動しても見えるようになっていると思います。)

4. Itemをクリックしたときの処理を追加する

セルをタップしたときの処理を書いていきます。簡単な処理であればCustomAdapter側に直接記述すれば良いと思いますが、今回はRealmの処理がActivityに依存しているため、CustomAdapterからActivity内の処理(編集、削除など)を呼び出したいです。

そこで、今回はClickListenerとなるinterfaceを定義し、そのインスタンスをActivityからAdapterに渡すことで実現します。

TaskAdapter.kt
class TaskAdapter(
    private val context: Context,
    private var taskList: OrderedRealmCollection<Task>?,
    private var listener: OnItemClickListener, // ---------追加----------
    private val autoUpdate: Boolean
) :
    RealmRecyclerViewAdapter<Task, TaskAdapter.TaskViewHolder>(taskList, autoUpdate) {

    override fun getItemCount(): Int = taskList?.size ?: 0

    override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
        val task: Task = taskList?.get(position) ?: return

        // ----------------追加---------------
        holder.container.setOnClickListener{
            listener.onItemClick(task)
        }
        // -----------------------------------
        holder.imageView.setImageResource(task.imageId)
        holder.contentTextView.text = task.content
        holder.dateTextView.text =
            SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.JAPANESE).format(task.createdAt)

    }

    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): TaskViewHolder {
        val v = LayoutInflater.from(context).inflate(R.layout.list_item, viewGroup, false)
        return TaskViewHolder(v)
    }

    class TaskViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val container : LinearLayout = view.container  // ---------追加----------
        val imageView: ImageView = view.imageView
        val contentTextView: TextView = view.contentTextView
        val dateTextView: TextView = view.dateTextView
    }

    // ----------------追加---------------
    interface OnItemClickListener {
        fun onItemClick(item: Task)
    }
    // -----------------------------------
}

MainActivity.kt
class MainActivity : AppCompatActivity() {

    /* 省略 */

    override fun onCreate(savedInstanceState: Bundle?) {
        /* 省略 */

        val adapter =
            TaskAdapter(this, taskList, object : TaskAdapter.OnItemClickListener {
                override fun onItemClick(item: Task) {
                    // クリック時の処理
                    Toast.makeText(applicationContext, item.content + "を削除しました", Toast.LENGTH_SHORT).show()
                    delete(item.id)
                }
            }, true)

        /* 省略 */

    }
    /* 省略 */

    fun update(id: String, content: String) {
        realm.executeTransaction {
            val task = realm.where(Task::class.java).equalTo("id", id).findFirst()
                ?: return@executeTransaction
            task.content = content
        }
    }

    fun update(task: Task, content: String) {
        realm.executeTransaction {
            task.content = content
        }
    }

    fun delete(id: String) {
        realm.executeTransaction {
            val task = realm.where(Task::class.java).equalTo("id", id).findFirst()
                ?: return@executeTransaction
            task.deleteFromRealm()
        }
    }

    fun delete(task: Task) {
        realm.executeTransaction {
            task.deleteFromRealm()
        }
    }

    fun deleteAll() {
        realm.executeTransaction {
            realm.deleteAll()
        }
    }

}

今回はセルがタップされたときにそれを削除し、それをToastで表示するようにしました。

これで一旦完成です。

終わりに

Realm周り結構ハマりどころがあるので、適宜調べながらやっていくと良いと思います。
また、SQLに慣れているのであればRoomを使うのもありなので、Realmが大変であれば別のものを採用するのもありだと思います。

参考: Room | Android Developers

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

AndroidのNFCでSuicaの履歴を読んでみる

はじめに

去年のアドベントカレンダーでAndroidのNFCでMifare Ultralightのタグを読み取る方法について以下の記事を書きました。

「ICカードだけじゃない!? AndroidのNFCで読み書きしてみた」
https://qiita.com/kurotsu/items/0748961a59a33a045d33

1年ぶりですが、続編ということで今度は皆さんにもおなじみのSuicaを使ってみたいと思います。
NFCやAndroidのAPIに関して基本的な事柄を去年の記事で解説しています。
また、画面遷移について同じ実装でSuicaの情報を読み取るところだけ追加していきますので、
一度、去年の記事を参照してみてください。

Suicaを読み取る実装の追加

タグを検出して、画面のタブを切替えてからFragment①に処理が切り替わるまでは同じです。
検出したタグによって処理を分ける部分を再度、掲載しておきます。

        // NDEF information
        if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)
                || NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {

            Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
            String[] techList = tag.getTechList();
            for (String tech : techList) {
                if(tech.equals("android.nfc.tech.NfcA")){
                    techFragment.setTechAFragment(intent);
                }
                if(tech.equals("android.nfc.tech.NfcB")){
                    techFragment.setTechBFragment(intent);
                }
                if(tech.equals("android.nfc.tech.NfcF")){
                    techFragment.setTechFFragment(intent);
                }
            }

            procNdef(intent);

        // Felica information
        } else if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)) {

            Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
            String[] techList = tag.getTechList();
            String techSelect = "";
            for (String tech : techList) {
                if(tech.equals("android.nfc.tech.NfcA")){
                    techFragment.setTechAFragment(intent);
                    techSelect = tech;
                }
                if(tech.equals("android.nfc.tech.NfcB")){
                    techFragment.setTechBFragment(intent);
                    techSelect = tech;
                }
                if(tech.equals("android.nfc.tech.NfcF")){
                    techFragment.setTechFFragment(intent);
                    techSelect = tech;
                }
            }

            // Felica Card
            if(techSelect.equals("android.nfc.tech.NfcF")){
                procFelica(intent);
            }
            if(techSelect.equals("android.nfc.tech.NfcB")){
                IsoDep isoDep = IsoDep.get(tag);
                if(isoDep != null){
                    procDriversCard(intent);
                }
            }
            // Mifare Classic Card
            if(techSelect.equals("android.nfc.tech.NfcA")){
                MifareClassic mfClassic = MifareClassic.get(tag);
                if(mfClassic != null){
                    procMfClassic(intent);
                }
            }

Suicaのカードをかざした場合はタグのtechListに"android.nfc.tech.NfcF"が設定されてきます。
procFelica(intent)の実装をして読み取っていきます。

/**
     * Felica Cardの処理
     *
     * @param intent
     */
    private void procFelica(final Intent intent) {

        byte[] idm = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID);
        Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
        NfcF nfcF = NfcF.get(tag);
        byte[] systemCode = nfcF.getSystemCode();
        StringBuilder sb = new StringBuilder();
        StringBuilder sb2 = new StringBuilder();
        StringBuilder sb3 = new StringBuilder();

        // Suica Transaction
        if(byteToInt(systemCode) == 0x0003){
            byte addr = 0;
            int serviceCode = 0x090f; // サービスコマンド(Suica履歴情報)
            byte[] serviceCmd = new byte[]{(byte) (serviceCode & 0xff), (byte) (serviceCode >> 8)};
            byte[] read_wo_encryption_command = new byte[]{(byte)0x06}; // 非暗号化領域読込コマンド read_wo_encryption_command:0x06
            byte[] data = new byte[]{
                    (byte) 0x01,            // サービス数
                    (byte) serviceCmd[0],   // サービスコード (little endian)
                    (byte) serviceCmd[1],
                    (byte) 0x01,            // 同時読み込みブロック数
                    (byte) 0x80, addr // ブロックリスト
            };
            int length = 16; // コマンド長(length(1)+ idm(8)+ cmd(1)+ data(6))

            ByteBuffer buff = ByteBuffer.allocate(length);
            byte byteLength = (byte)length;
            buff.put(byteLength).put(read_wo_encryption_command).put(idm).put(data);
            byte[] tranCmd = buff.array();

            byte[] response;
            Long remain;
            SimpleDateFormat sdf;
            String strDate;
            byte[] contents;
            NumberFormat nf = NumberFormat.getCurrencyInstance();
            nf.setMaximumFractionDigits(0);

            try {
                nfcF.connect();
                response = nfcF.transceive(tranCmd);
                while(response != null){
                    if(response.length < 13){
                        break;
                    }
                    if((response[13] & 0xff) == 0xc7 || (response[13] & 0xff) == 0xc8){
                        sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss",Locale.JAPANESE);
                        strDate = sdf.format(getProccessDate(byteToInt(new byte[]{response[17], response[18]}),byteToInt(new byte[]{response[19], response[20]})));
                    } else {
                        sdf = new SimpleDateFormat("yyyy/MM/dd",Locale.JAPANESE);
                        strDate = sdf.format(getProccessDate(byteToInt(new byte[]{response[17], response[18]}),0));
                    }
                    sb.append("日時:" + strDate + "\n");

                    int cType = byteToInt(new byte[]{response[13]});
                    sb.append("機器:" + getConsoleType(cType) + "\n");

                    int proc = byteToInt(new byte[]{response[14]});
                    sb.append("処理:" + getProcessType(proc) + "\n");

                    remain = Long.valueOf((byteToInt(new byte[]{response[24], response[23]})));
                    sb.append("残高:" + nf.format(remain) + "\n");
                    sb.append("残高(b):");
                    sb.append(String.format("%02X", response[24]));
                    sb.append(String.format("%02X", response[23]));
                    sb.append("\n");

                    contents = Arrays.copyOfRange(response, 10, response.length);
                    for (byte b : contents) {
                        sb.append(String.format("%02X", b));
                    }
                    sb.append("\n");
                    sb.append("---------------------------------------------------------\n");

                    addr++;
                    tranCmd[tranCmd.length-1] = addr;
                    response = nfcF.transceive(tranCmd);
                }
            } catch(Exception e){
                e.printStackTrace();
            } finally {
                try {
                    nfcF.close();
                } catch(Exception e){
                    e.printStackTrace();
                }
            }

        // nanaco Transaction
        } else if(byteToInt(systemCode) == 0x04c7){
      // TODO: nanaco implementation    

        } else {

            sb.append("none\n");
        }
        setFelicaTransactionBody(sb.toString());

        // System Code List
        byte[] command_request_systemcode = new byte[]{(byte)0x0c}; // SystemCode検索コマンド command_request_systemcode:0x0c
        int length = 10; // コマンド長(length(1)+ idm(8)+ cmd(1))

        ByteBuffer buff = ByteBuffer.allocate(length);
        byte byteLength = (byte)length;
        buff.put(byteLength).put(command_request_systemcode).put(idm);
        byte[] systemCodeCmd = buff.array();

        byte[] response;

        try {
            nfcF.connect();
            response = nfcF.transceive(systemCodeCmd);
            int array_length = (int)response[10];
            byte[] serviceCodes;
            for (int i=0; i < array_length; i++) {
                serviceCodes = Arrays.copyOfRange(response, 11+i*2, 13+i*2);
                for (byte b : serviceCodes) {
                    sb2.append(String.format("%02X", b));
                }
                sb2.append("\n");
            }

        } catch(Exception e){
            e.printStackTrace();
        } finally {
            try {
                nfcF.close();
            } catch(Exception e){
                e.printStackTrace();
            }
        }
        setFelicaSystemCodeBody(sb2.toString());


        // Service Command List
        byte[] command_search_servicecode = new byte[]{(byte)0x0a}; // SystemCode検索コマンド command_search_servicecode:0x0a
        length = 12; // コマンド長(length(1)+ idm(8)+ cmd(1) + index(2))
        int addr = 0;

        byte[] indexByte = new byte[]{(byte)(addr & 0xff), (byte)(addr >> 8)};

        buff = ByteBuffer.allocate(length);
        byteLength = (byte)length;
        buff.put(byteLength).put(command_search_servicecode).put(idm).put(indexByte);
        byte[] serviceCodeCmd = buff.array();
        byte[] contents;
        byte[] serviceCode = new byte[2];

        try {
            nfcF.connect();
            response = nfcF.transceive(serviceCodeCmd);
            while(response != null){

                contents = Arrays.copyOfRange(response, 10, response.length);
                if (contents.length != 2 && contents.length != 4) break; // 2 or 4 バイトじゃない場合終了
                if (contents.length == 2) {
                    if (contents[0] == (byte)0xff && contents[1] == (byte)0xff) break; // FFFF が終了コード

                    // little endian
                    serviceCode[0] = contents[1];
                    serviceCode[1] = contents[0];
                    for (byte b : serviceCode) {
                        sb3.append(String.format("%02X", b));
                    }
                    int accessInfo = serviceCode[1] & 0x3F; // 下位6bitがアクセス情報
                    switch (accessInfo) {
                        case 0x09: sb3.append(" 固定長RW"); break; // RW: ReadWrite
                        case 0x0b: sb3.append(" 固定長RO"); break; // RO: ReadOnly
                        case 0x0d: sb3.append(" 循環RW"); break;
                        case 0x0f: sb3.append(" 循環RO"); break;
                        case 0x11: sb3.append(" 加減算直接"); break;
                        case 0x13: sb3.append(" 加減算戻入"); break;
                        case 0x15: sb3.append(" 加減算減算"); break;
                        case 0x17: sb3.append(" 加減算RO"); break;
                        case 0x08: sb3.append(" 固定長RW(Locked)"); break; // RW: ReadWrite
                        case 0x0a: sb3.append(" 固定長RO(Locked)"); break; // RO: ReadOnly
                        case 0x0c: sb3.append(" 循環RW(Locked)"); break;
                        case 0x0e: sb3.append(" 循環RO(Locked)"); break;
                        case 0x10: sb3.append(" 加減算直接(Locked)"); break;
                        case 0x12: sb3.append(" 加減算戻入(Locked)"); break;
                        case 0x14: sb3.append(" 加減算減算(Locked)"); break;
                        case 0x16: sb3.append(" 加減算RO(Locked)"); break;
                    }

                    sb3.append("\n");
                }

                addr++;
                serviceCodeCmd[serviceCodeCmd.length-2] = (byte)(addr & 0xff);
                serviceCodeCmd[serviceCodeCmd.length-1] = (byte)(addr >> 8);
                response = nfcF.transceive(serviceCodeCmd);
            }

        } catch(Exception e){
            e.printStackTrace();
        } finally {
            try {
                nfcF.close();
            } catch(Exception e){
                e.printStackTrace();
            }
        }
        setFelicaServiceCodeBody(sb3.toString());

    }

Felicaのカードにはシステムコードと呼ばれるカードの種類によって決められたコードが規定されています。
Suicaは”0003”です。NanacoやWAONなどFelicaを採用しているカードにもそれぞれシステムコードが割り振られています。
それから、カードの中の情報についてはアクセス先を表すサービスコードが設定されています。
Suicaのカードでチャージをしたり、タッチして品物を購入したときの履歴情報は”090f”です。
サービスコードやidmを利用して、AndroidのnfcFのAPIを使ってコマンドを送信してレスポンスを受け取ります。
ブロックアドレスを変更しながら、最大20件の履歴情報を読み取ることが出来ます。

「// System Code List」のコメントから先はカードに設定されているシステムコードの一覧を表示しています。
Suicaでは”0003”の他にもコードが設定されているようです。
「// Service Command List」のコメントから先はカードに設定されているサービスコードの一覧を表示しています。
この情報からアクセスできる情報のサービスコードを確認することができます。
履歴情報は”090f”は循環型のデータ構造でReadOnlyで非暗号化の領域ということがわかります。

取得した情報を設定している処理が下記になります。

    /**
     * Felica履歴 の表示 - ページ
     *
     * @param page
     */
    private void setFelicaTransactionBody(String page) {
        map = new HashMap<String, String>();
        map.put("Title", "Transaction");
        groupData.add(map);
        map = new HashMap<String, String>();
        map.put("Text", page);
        ArrayList<HashMap<String, String>> list = new ArrayList<HashMap<String, String>>();
        list.add(map);
        childData.add(list);

        lv.expandGroup(0);
    }

    /**
     * Felica System Code の一覧 - ページ
     *
     * @param page
     */
    private void setFelicaSystemCodeBody(String page) {
        map = new HashMap<String, String>();
        map.put("Title", "System Code List");
        groupData.add(map);
        map = new HashMap<String, String>();
        map.put("Text", page);
        ArrayList<HashMap<String, String>> list = new ArrayList<HashMap<String, String>>();
        list.add(map);
        childData.add(list);

        lv.expandGroup(1);
    }

    /**
     * Felica Service Code の一覧 - ページ
     *
     * @param page
     */
    private void setFelicaServiceCodeBody(String page) {
        map = new HashMap<String, String>();
        map.put("Title", "Service Code List");
        groupData.add(map);
        map = new HashMap<String, String>();
        map.put("Text", page);
        ArrayList<HashMap<String, String>> list = new ArrayList<HashMap<String, String>>();
        list.add(map);
        childData.add(list);

        lv.expandGroup(2);
    }

履歴情報のタイプを変換するメソッドが下記となります。
コメントがあるとおり、有志の方が調べてくれた情報の下記サイトを参考にしています。
http://sourceforge.jp/projects/felicalib/wiki/suica
だいぶ前に実装したものなので、適宜情報を最新にしてみてください。

    /**
     * 機器種別を取得します
     *
     * <pre>
     * http://sourceforge.jp/projects/felicalib/wiki/suicaを参考にしています
     * </pre>
     *
     * @param cType
     *            コンソールタイプをセット
     * @return String 機器タイプが文字列で戻ります
     */
    public static final String getConsoleType(int cType) {
        switch (cType & 0xff) {
            case 0x03:
                return "精算機";
            case 0x04:
                return "携帯型端末";
            case 0x05:
                return "等車載端末"; // bus
            case 0x07:
                return "券売機";
            case 0x08:
                return "券売機";
            case 0x09:
                return "入金機(クイックチャージ機)";
            case 0x12:
                return "券売機(東京モノレール)";
            case 0x13:
                return "券売機等";
            case 0x14:
                return "券売機等";
            case 0x15:
                return "券売機等";
            case 0x16:
                return "改札機";
            case 0x17:
                return "簡易改札機";
            case 0x18:
                return "窓口端末";
            case 0x19:
                return "窓口端末(みどりの窓口)";
            case 0x1a:
                return "改札端末";
            case 0x1b:
                return "携帯電話";
            case 0x1c:
                return "乗継清算機";
            case 0x1d:
                return "連絡改札機";
            case 0x1f:
                return "簡易入金機";
            case 0x46:
                return "VIEW ALTTE";
            case 0x48:
                return "VIEW ALTTE";
            case 0xc7:
                return "物販端末"; // sales
            case 0xc8:
                return "自販機"; // sales
            default:
                return "???";
        }
    }

    /**
     * 処理種別を取得します
     *
     * <pre>
     * http://sourceforge.jp/projects/felicalib/wiki/suicaを参考にしています
     * </pre>
     *
     * @param proc
     *            処理タイプをセット
     * @return String 処理タイプが文字列で戻ります
     */
    public static final String getProcessType(int proc) {
        switch (proc & 0xff) {
            case 0x01:
                return "運賃支払(改札出場)";
            case 0x02:
                return "チャージ";
            case 0x03:
                return "券購(磁気券購入";
            case 0x04:
                return "精算";
            case 0x05:
                return "精算(入場精算)";
            case 0x06:
                return "窓出(改札窓口処理)";
            case 0x07:
                return "新規(新規発行)";
            case 0x08:
                return "控除(窓口控除)";
            case 0x0d:
                return "バス(PiTaPa系)"; // byBus
            case 0x0f:
                return "バス(IruCa系)"; // byBus
            case 0x11:
                return "再発(再発行処理)";
            case 0x13:
                return "支払(新幹線利用)";
            case 0x14:
                return "入A(入場時オートチャージ)";
            case 0x15:
                return "出A(出場時オートチャージ)";
            case 0x1f:
                return "入金(バスチャージ)"; // byBus
            case 0x23:
                return "券購 (バス路面電車企画券購入)"; // byBus
            case 0x46:
                return "物販"; // sales
            case 0x48:
                return "特典(特典チャージ)";
            case 0x49:
                return "入金(レジ入金)"; // sales
            case 0x4a:
                return "物販取消"; // sales
            case 0x4b:
                return "入物 (入場物販)"; // sales
            case 0xc6:
                return "物現 (現金併用物販)"; // sales
            case 0xcb:
                return "入物 (入場現金併用物販)"; // sales
            case 0x84:
                return "精算 (他社精算)";
            case 0x85:
                return "精算 (他社入場精算)";
            default:
                return "???";
        }
    }

Fragment②ではFelicaの技術情報を追加します。

        byte[] idm = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID);
        sb1.append("ID:");
        if (idm != null){
            for (byte b : idm) {
                sb1.append(String.format("%02X", b));
            }
        }
        sb1.append("\n");

        Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
        NfcF nfcF = NfcF.get(tag);

        byte[] manufacturer = nfcF.getManufacturer();
        if(manufacturer != null){
            sb1.append("PMm : ");
            for (byte b : manufacturer) {
                sb1.append(String.format("%02X", b));
            }
            sb1.append("\n");
        }

        sb2.append("JIS X 6319-4 compatible\n");
        byte[] data = new byte[1000];
        try {
            nfcF.connect();
            nfcF.transceive(data);
        } catch(Exception e){
            e.printStackTrace();
        } finally {
            try {
                nfcF.close();
            } catch(Exception e){
                e.printStackTrace();
            }
        }
        int tran_length = nfcF.getMaxTransceiveLength();

        if(tran_length < 0){
            sb2.append("▶Maximum transceive length: unknown\n");
        } else {
            sb2.append("▶Maximum transceive length: " + Integer.valueOf(tran_length)  + "bytes\n");
        }
        int timeout = nfcF.getTimeout();
        sb2.append("▶Maximum transceive time-out: " + Integer.valueOf(timeout) + "ms\n");

        byte[] systemCode = nfcF.getSystemCode();
        if(systemCode != null){
            sb3.append("System Code : ");
            for (byte b : systemCode) {
                sb3.append(String.format("%02X", b));
            }
            sb3.append("\n");
        }
        if(NdefReadFragment.byteToInt(systemCode) == 0x0003){
            sb3.append("Card Type : Suica\n");
        }


        String[] techList = tag.getTechList();
        for(int i = 0; i <= techList.length - 1; i++){
            sb4.append("▶" + techList[i]);
            if(i < techList.length - 1) sb4.append("\n");
        }

        setTechFPage(sb1.toString(),sb2.toString(),sb3.toString(),sb4.toString());

これで実装の説明は終わりです。

Suicaの履歴情報を読み取ってみる

いよいよ実際にSuicaを読み取りたいと思います。

Suicaをアプリで読み取って際の分割写真を張り付けています。
SCANタブの情報です。

Screenshot_20191213-133159.png
Screenshot_20191213-133232.png
Screenshot_20191213-133302.png
Screenshot_20191213-133317.png
Screenshot_20191213-133339.png

TECHタブの情報が下記になります。

Screenshot_20191213-133352.png

おわりに

実装の細部などは端折ってしまっていますが、皆さんで考えながら実装してみてください。
皆さんの手元にあるカードでFelicaを採用しているものもあるかと思います。
システムコードやサービスコードの情報がこの実装で読み取ることが出来ると思いますので、
実際にかざしてみて確認してみてください。

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

Kotlinで本番用と開発・検証用の実装を分ける場合のTIPS

以下のようなRepositoryをinterfaceとして定義したとする。

interface HogeRepository {
    suspend fun hoge()
    suspend fun piyo()
}

本番用のリポジトリとして HogeRepository を実装する。

class HogeRepositoryProduction : HogeRepository {
    override suspend fun hoge() = withContext(io) {}
    override suspend fun piyo() = withContext(io) {}
}

開発や検証用のリポジトリは本番用のリポジトリの実装を受け継ぎつつ、必要なところだけ書き換える。

class HogeRepositoryDevelopment(
        private val production: HogeRepositoryProduction
) : HogeRepository by production {
    override suspend fun piyo() = withContext(io) {}
}

これで開発や検証用のリポジトリを実装する際に、本番用と同じコードを記述する必要がない。

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

Kotlinでinterfaceの実装を本番用と開発・検証用などで分ける場合のTIPS

以下のようなRepositoryをinterfaceとして定義したとする。

interface HogeRepository {
    suspend fun hoge()
    suspend fun piyo()
}

本番用のリポジトリとして HogeRepository を実装する。

class HogeRepositoryProduction : HogeRepository {
    override suspend fun hoge() = withContext(io) {}
    override suspend fun piyo() = withContext(io) {}
}

開発や検証用のリポジトリは本番用のリポジトリの実装を受け継ぎつつ、必要なところだけ書き換える。

class HogeRepositoryDevelopment(
        private val production: HogeRepositoryProduction
) : HogeRepository by production {
    override suspend fun piyo() = withContext(io) {}
}

これで開発や検証用のリポジトリを実装する際に、本番用と同じコードを記述する必要がない。

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

monacaでandroidのプッシュアイコンを設定する方法

config.xmlに以下の内容を記述

<platform name="android">
    <resource-file src="/res/android/hoge.png" target="/res/drawable/hoge.png"/>
    <config-file target="AndroidManifest.xml" parent="./application">
        <meta-data android:name="smallIcon" android:resource="@drawable/hoge"/>
        <meta-data android:name="smallIconColor" android:value="0x01a2c1"/>
    </config-file>
</platform>

android:value="0x01a2c1"のところで、16進数で色番を指定すると色がつけられるよ!

monacaとのやりとり

問合せ

現在、アプリの設定でアイコンを設定しておりますが、ストアに表示されるアイコンと同一のものが通知でも表示されます。
その場合、白い四角で表示されます。
解決策としてはnotification用のアイコンを別途指定する必要がありそうですが、設定方法がわかりません。
ご教授のほど何卒、宜しくお願い致します。

参考資料
https://qiita.com/syarihu/items/95788cbab9b63100c4fb

回答

いつもお世話になっております。
Monacaサポート担当です。
お問い合わせいただき、誠にありがとうございます。

お問い合わせの件について、回答いたします。

Monacaバックエンドやニフクラ mobile backendからプッシュ通知を行った場合は、
「Androidアプリ設定」の「アイコン」に設定されている画像が使用されます。

Androidでは、Android 5.0から通知アイコンのファイル仕様が変更になっております。

Android 5.0から取り入れられたマテリアルデザインとしてアイコンが処理されるため、
これまで表示されていた通知アイコンは、「白い四角」のアイコンとして表示される場合があります。

白いアイコンは、アイコン画像のアルファチャネル(不透明な部分)を参照してOS側が作成します。
そのためアイコン画像が不透明な部分だけの場合、「白い四角」なアイコンとして表示されます。

Monacaバックエンドやニフクラ mobile backendからプッシュ通知を行った場合も、
基本的に、Android 5.0以降では「白い四角」なアイコンが表示されます。

端末によっては、端末の独自仕様として、
通常のAndroidの仕様とは異なる表示になるものもございます。

現状、Monaca側で行えるプッシュ通知時の通知領域の画像につきましては、
「Androidアプリ設定」の「アイコン」に、透過画像を設定していただく形となります。

ご了承のほど、よろしくお願いいたします。

以上、何卒よろしくお願いいたします。
Monacaサポート担当

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

私がアプリ開発のSESで、面談時に地雷案件をなるべく避けるための確認ポイント7選

私は、客先常駐でアプリを開発、保守するのをメインに5年ほどお仕事してきました。
iOSとAndroidのどちらかを担当して、機能やスケジュールの提案をしつつ、もくもくと開発していることが多いエンジニアです。

常駐のため、現場が変わるごとに面談があり(法律なんか知らん)、その度にやばそうな雰囲気の出てる案件は基本的に避けるようにしています。
現場に入る前の面談で、これを確認すれば、地雷案件を少し回避できるポイントがなんとなくわかってきたので、メモっておきます。
※あくまで個人的な指標であって、なんの根拠もないです!!

保守案件の場合は、アプリのストア評価を確認

保守案件で、ストアの評価が低いアプリは可能な限り参画を回避します。

理由は、

  • ストアの評価が低いものは、内部の品質が基本的に低い
  • 新たな開発はできずに、既存バグの修正に追われて疲弊する
  • ただのバグ修正ではスキルがつきにくい
  • クソコードを見ると発狂する(コントローラにAPIリクエストが入っているとか無理)
  • ひどい場合、gitすら使ってない
  • その他色々

だからです。
何より、過去に低評価アプリを3つほど、保守・機能追加を担当して、すごく苦労したので

アプリのアーキテクチャーを確認

  • 全てをControllerに実装するアーキテクチャ
  • MVC
  • MVVM
  • VIPER
  • CleanArchitecture
  • など

色々とありますが、開発するアプリのアーキテクチャが決まっている場合があるので、確認します。

保守開発の案件で、既存のアーキテクチャを教えていただけることがあるのですが、
なぜか「CleanArchitecture」を使用してると言われるプロジェクトは、クラス名だけCleanArchitectureで、中身はぐちゃぐちゃの地雷が多いです。

経験上、「CleanArchitecture」と「全てをControllerに実装するアーキテクチャ」は避けたい対象となっています。

常駐先の会社さんにアプリ開発をできるプロパーさんがいるか確認

受託開発の新規作成案件で、アプリを開発できるエンジニアさんがいない会社が受けた場合、要件定義や見積もりでやらかしている可能性がかなり上がります。
Web画面を作る感覚でアプリの見積もりをしてしまった案件の場合は、開発工数がそもそも足りていない場合が多くあるイメージです。

見積もりが多い分には、常駐する側としては問題ないのでが、やらかしている可能性が高い案件の場合が多いので、なるべく避けるのがおすすめです。

開発者を複数人募集している案件は、他のメンバーのスキルを確認

最近の案件は、1アプリを2~3人くらいで新規開発をする案件がそこそこあるイメージです。
ただし、複数人開発と言えど、

  • 新人さん(2年未満)
  • アプリを一人で作ったことのない人

が相方として参画しそうな場合は、申し訳ないですが確実に避けるようにしています。
(他の人の面倒を見るのは避けたい。ただし、面倒を見る費用をもらえる場合は別途相談)

また、開発規模の割に人数が多い案件の場合は、
スキルの低い人間を集めて、クライアントへの見積もりをカサ増ししている案件が多いです。
(できる人が1人で2ヶ月できるのを、4人で半年とか)
結局、できる人間が全て作るハメになるので、避けるようにしています。

フロアのエンジニアの残業状況を確認

面談の際に常駐先のエンジニアの残業状態を確認します。
残業をしている人が多い場合は、だらだら仕事する癖がついている or 仕事量がおかしい 現場の可能性が高いので、なるべく避けます。

質問は、自分から質問すると残業を避けたがるエンジニアのイメージがつく気がするので、営業から質問してもらえるようにお願いするのがベストです。

スクラム開発の場合に、スクラムの前提を満たしているか確認

最近流行っているスクラム開発を取り入れている案件がそこそこ出てきました。
ただし、スクラムをやる上で、前提条件を満たしていない案件が多いので注意が必要です。

  • 全機機能ができる納期がある????
  • スキルがバラバラすぎる
  • クライアントがスクラム開発に乗り気ではない(POさんがいないとか)
  • スクラムを銀の弾丸と思っている人がいる(スクラムだから開発に成功するとか)

(前提条件を満たしていない時点で、スクラムではないと思いますが。。。)

また、これは個人的な都合ですが、以下の点でスクラム開発はちょっとやりにくいところがあります。

  • 作業見積もりをごまかしにくい
  • 最初に一気に開発して、開発期間の最後の方でゆっくりすることがしにくい(開発期間の余ったところでだらだらするのが趣味のため)
  • 全員バラバラの所属で、ガッチリとしたチームプレーはちょっとめんどい

色々、ネガティブなことを書いてますが、うまくはまっているスクラム開発は色々と新しいことができて楽しいのでおすすめです!!

開発環境の確認

  • Mac/PCの簡単なスペック(メモリ8GBじゃ辛い)
  • iPhoneなどの実機の有無

などを念の為確認すると、あとで開発中にイライラするのを防げるかもしれないです。

まとめ

色々書きましたが、結局は現場に参画してみないとわかりません。

わざわざ、大変だったり辛い現場に行くのは勿体無いので、
お金をもらえながら、楽しくてスキルのつける現場になるべく行けるように、これからも頑張って面談していく予定です。

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

iOS/Androidアプリ開発のSESで、面談時に地雷案件をなるべく避けるための確認ポイント7選

私は、客先常駐でアプリを開発、保守するのをメインに5年ほどお仕事してきました。
iOSとAndroidのどちらかを担当して、機能やスケジュールの提案をしつつ、もくもくと開発していることが多いエンジニアです。

常駐のため、現場が変わるごとに面談があり(法律なんか知らん)、その度にやばそうな雰囲気の出てる案件は基本的に避けるようにしています。
現場に入る前の面談で、これを確認すれば、地雷案件を少し回避できるポイントがなんとなくわかってきたので、メモっておきます。
※あくまで個人的な指標であって、なんの根拠もないです!!

保守案件の場合は、アプリのストア評価を確認

保守案件で、ストアの評価が低いアプリは可能な限り参画を回避します。

理由は、

  • ストアの評価が低いものは、内部の品質が基本的に低い
  • 新たな開発はできずに、既存バグの修正に追われて疲弊する
  • ただのバグ修正ではスキルがつきにくい
  • クソコードを見ると発狂する(コントローラにAPIリクエストが入っているとか無理)
  • ひどい場合、gitすら使ってない
  • その他色々

だからです。
何より、過去に低評価アプリを3つほど、保守・機能追加を担当して、すごく苦労したので

アプリのアーキテクチャーを確認

  • 全てをControllerに実装するアーキテクチャ
  • MVC
  • MVVM
  • VIPER
  • CleanArchitecture
  • など

色々とありますが、開発するアプリのアーキテクチャが決まっている場合があるので、確認します。

保守開発の案件で、既存のアーキテクチャを教えていただけることがあるのですが、
なぜか「CleanArchitecture」を使用してると言われるプロジェクトは、クラス名だけCleanArchitectureで、中身はぐちゃぐちゃの地雷が多いです。

経験上、「CleanArchitecture」と「全てをControllerに実装するアーキテクチャ」は避けたい対象となっています。

常駐先の会社さんにアプリ開発をできるプロパーさんがいるか確認

受託開発の新規作成案件で、アプリを開発できるエンジニアさんがいない会社が受けた場合、要件定義や見積もりでやらかしている可能性がかなり上がります。
Web画面を作る感覚でアプリの見積もりをしてしまった案件の場合は、開発工数がそもそも足りていない場合が多くあるイメージです。

見積もりが多い分には、常駐する側としては問題ないのでが、やらかしている可能性が高い案件の場合が多いので、なるべく避けるのがおすすめです。

開発者を複数人募集している案件は、他のメンバーのスキルを確認

最近の案件は、1アプリを2~3人くらいで新規開発をする案件がそこそこあるイメージです。
ただし、複数人開発と言えど、

  • 新人さん(2年未満)
  • アプリを一人で作ったことのない人

が相方として参画しそうな場合は、申し訳ないですが確実に避けるようにしています。
(他の人の面倒を見るのは避けたい。ただし、面倒を見る費用をもらえる場合は別途相談)

また、開発規模の割に人数が多い案件の場合は、
スキルの低い人間を集めて、クライアントへの見積もりをカサ増ししている案件が多いです。
(できる人が1人で2ヶ月できるのを、4人で半年とか)
結局、できる人間が全て作るハメになるので、避けるようにしています。

SIer系は避ける

会社さんによりますが、古いルールとかに縛られていることがあるので、近づかないほうがいいイメージ。
特にスーツ必須のところは、ちょっと怖い。
※うちは、違うぜ!だ感じの会社さんはごめんなさい。経験上は、どうしても多いのです。

フロアのエンジニアの残業状況を確認

面談の際に常駐先のエンジニアの残業状態を確認します。
残業をしている人が多い場合は、だらだら仕事する癖がついている or 仕事量がおかしい 現場の可能性が高いので、なるべく避けます。

質問は、自分から質問すると残業を避けたがるエンジニアのイメージがつく気がするので、営業から質問してもらえるようにお願いするのがベストです。

スクラム開発の場合に、スクラムの前提を満たしているか確認

最近流行っているスクラム開発を取り入れている案件がそこそこ出てきました。
ただし、スクラムをやる上で、前提条件を満たしていない案件が多いので注意が必要です。

  • 全機能ができる納期がある????
  • スキルがバラバラすぎる
  • クライアントがスクラム開発に乗り気ではない(POさんがいないとか)
  • スクラムを銀の弾丸と思っている人がいる(スクラムだから開発に成功するとか)

(前提条件を満たしていない時点で、スクラムではないと思いますが。。。)

また、これは個人的な都合ですが、以下の点でスクラム開発はちょっとやりにくいところがあります。

  • 作業見積もりをごまかしにくい
  • 最初に一気に開発して、開発期間の最後の方でゆっくりすることがしにくい(開発期間の余ったところでだらだらするのが趣味のため)
  • 全員バラバラの所属で、ガッチリとしたチームプレーはちょっとめんどい

色々、ネガティブなことを書いてますが、うまくはまっているスクラム開発は色々と新しいことができて楽しいのでおすすめです!!

開発環境の確認

  • Mac/PCの簡単なスペック(メモリ8GBじゃ辛い)
  • iPhoneなどの実機の有無

などを念の為確認すると、あとで開発中にイライラするのを防げるかもしれないです。

まとめ

色々書きましたが、結局は現場に参画してみないとわかりません。

わざわざ、大変だったり辛い現場に行くのは勿体無いので、
お金をもらえながら、楽しくてスキルのつける現場になるべく行けるように、これからも頑張って面談していく予定です。

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

私がiOS/Androidアプリ開発のSESで、面談時に地雷案件をなるべく避けるための確認ポイント7選

私は、客先常駐でアプリを開発、保守するのをメインに5年ほどお仕事してきました。
iOSとAndroidのどちらかを担当して、機能やスケジュールの提案をしつつ、もくもくと開発していることが多いエンジニアです。

常駐のため、現場が変わるごとに面談があり(法律なんか知らん)、その度にやばそうな雰囲気の出てる案件は基本的に避けるようにしています。
現場に入る前の面談で、これを確認すれば、地雷案件を少し回避できるポイントがなんとなくわかってきたので、メモっておきます。
※あくまで個人的な指標であって、なんの根拠もないです!!

保守案件の場合は、アプリのストア評価を確認

保守案件で、ストアの評価が低いアプリは可能な限り参画を回避します。

理由は、

  • ストアの評価が低いものは、内部の品質が基本的に低い
  • 新たな開発はできずに、既存バグの修正に追われて疲弊する
  • ただのバグ修正ではスキルがつきにくい
  • クソコードを見ると発狂する(コントローラにAPIリクエストが入っているとか無理)
  • ひどい場合、gitすら使ってない
  • その他色々

だからです。
何より、過去に低評価アプリを3つほど、保守・機能追加を担当して、すごく苦労したので

アプリのアーキテクチャーを確認

  • 全てをControllerに実装するアーキテクチャ
  • MVC
  • MVVM
  • VIPER
  • CleanArchitecture
  • など

色々とありますが、開発するアプリのアーキテクチャが決まっている場合があるので、確認します。

保守開発の案件で、既存のアーキテクチャを教えていただけることがあるのですが、
なぜか「CleanArchitecture」を使用してると言われるプロジェクトは、クラス名だけCleanArchitectureで、中身はぐちゃぐちゃの地雷が多いです。

経験上、「CleanArchitecture」と「全てをControllerに実装するアーキテクチャ」は避けたい対象となっています。

常駐先の会社さんにアプリ開発をできるプロパーさんがいるか確認

受託開発の新規作成案件で、アプリを開発できるエンジニアさんがいない会社が受けた場合、要件定義や見積もりでやらかしている可能性がかなり上がります。
Web画面を作る感覚でアプリの見積もりをしてしまった案件の場合は、開発工数がそもそも足りていない場合が多くあるイメージです。

見積もりが多い分には、常駐する側としては問題ないのでが、やらかしている可能性が高い案件の場合が多いので、なるべく避けるのがおすすめです。

開発者を複数人募集している案件は、他のメンバーのスキルを確認

最近の案件は、1アプリを2~3人くらいで新規開発をする案件がそこそこあるイメージです。
ただし、複数人開発と言えど、

  • 新人さん(2年未満)
  • アプリを一人で作ったことのない人

が相方として参画しそうな場合は、申し訳ないですが確実に避けるようにしています。
(他の人の面倒を見るのは避けたい。ただし、面倒を見る費用をもらえる場合は別途相談)

また、開発規模の割に人数が多い案件の場合は、
スキルの低い人間を集めて、クライアントへの見積もりをカサ増ししている案件が多いです。
(できる人が1人で2ヶ月できるのを、4人で半年とか)
結局、できる人間が全て作るハメになるので、避けるようにしています。

SIer系は避ける

会社さんによりますが、古いルールとかに縛られていることがあるので、近づかないほうがいいイメージ。
特にスーツ必須のところは、ちょっと怖い。
※うちは、違うぜ!だ感じの会社さんはごめんなさい。経験上は、どうしても多いのです。

フロアのエンジニアの残業状況を確認

面談の際に常駐先のエンジニアの残業状態を確認します。
残業をしている人が多い場合は、だらだら仕事する癖がついている or 仕事量がおかしい 現場の可能性が高いので、なるべく避けます。

質問は、自分から質問すると残業を避けたがるエンジニアのイメージがつく気がするので、営業から質問してもらえるようにお願いするのがベストです。

スクラム開発の場合に、スクラムの前提を満たしているか確認

最近流行っているスクラム開発を取り入れている案件がそこそこ出てきました。
ただし、スクラムをやる上で、前提条件を満たしていない案件が多いので注意が必要です。

  • 全機能ができる納期がある????
  • スキルがバラバラすぎる
  • クライアントがスクラム開発に乗り気ではない(POさんがいないとか)
  • スクラムを銀の弾丸と思っている人がいる(スクラムだから開発に成功するとか)

(前提条件を満たしていない時点で、スクラムではないと思いますが。。。)

また、これは個人的な都合ですが、以下の点でスクラム開発はちょっとやりにくいところがあります。

  • 作業見積もりをごまかしにくい
  • 最初に一気に開発して、開発期間の最後の方でゆっくりすることがしにくい(開発期間の余ったところでだらだらするのが趣味のため)
  • 全員バラバラの所属で、ガッチリとしたチームプレーはちょっとめんどい

色々、ネガティブなことを書いてますが、うまくはまっているスクラム開発は色々と新しいことができて楽しいのでおすすめです!!

開発環境の確認

  • Mac/PCの簡単なスペック(メモリ8GBじゃ辛い)
  • iPhoneなどの実機の有無

などを念の為確認すると、あとで開発中にイライラするのを防げるかもしれないです。

まとめ

色々書きましたが、結局は現場に参画してみないとわかりません。

わざわざ、大変だったり辛い現場に行くのは勿体無いので、
お金をもらえながら、楽しくてスキルのつける現場になるべく行けるように、これからも頑張って面談していく予定です。

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

SESで、面談時に地雷案件をなるべく避けるための確認ポイント7選(iOS/Androidアプリ開発)

私は、客先常駐でアプリを開発、保守するのをメインに5年ほどお仕事してきました。
iOSとAndroidのどちらかを担当して、機能やスケジュールの提案をしつつ、もくもくと開発していることが多いエンジニアです。

常駐のため、現場が変わるごとに面談があり(法律なんか知らん)、その度にやばそうな雰囲気の出てる案件は基本的に避けるようにしています。
現場に入る前の面談で、これを確認すれば、地雷案件を少し回避できるポイントがなんとなくわかってきたので、メモっておきます。
※あくまで個人的な指標であって、なんの根拠もないです!!

保守案件の場合は、アプリのストア評価を確認

保守案件で、ストアの評価が低いアプリは可能な限り参画を回避します。

理由は、

  • ストアの評価が低いものは、内部の品質が基本的に低い
  • 新たな開発はできずに、既存バグの修正に追われて疲弊する
  • ただのバグ修正ではスキルがつきにくい
  • クソコードを見ると発狂する(コントローラにAPIリクエストが入っているとか無理)
  • ひどい場合、gitすら使ってない
  • その他色々

だからです。
何より、過去に低評価アプリを3つほど、保守・機能追加を担当して、すごく苦労したので

アプリのアーキテクチャーを確認

  • 全てをControllerに実装するアーキテクチャ
  • MVC
  • MVVM
  • VIPER
  • CleanArchitecture
  • など

色々とありますが、開発するアプリのアーキテクチャが決まっている場合があるので、確認します。

保守開発の案件で、既存のアーキテクチャを教えていただけることがあるのですが、
なぜか「CleanArchitecture」を使用してると言われるプロジェクトは、クラス名だけCleanArchitectureで、中身はぐちゃぐちゃの地雷が多いです。

経験上、「CleanArchitecture」と「全てをControllerに実装するアーキテクチャ」は避けたい対象となっています。

常駐先の会社さんにアプリ開発をできるプロパーさんがいるか確認

受託開発の新規作成案件で、アプリを開発できるエンジニアさんがいない会社が受けた場合、要件定義や見積もりでやらかしている可能性がかなり上がります。
Web画面を作る感覚でアプリの見積もりをしてしまった案件の場合は、開発工数がそもそも足りていない場合が多くあるイメージです。

見積もりが多い分には、常駐する側としては問題ないのでが、やらかしている可能性が高い案件の場合が多いので、なるべく避けるのがおすすめです。

開発者を複数人募集している案件は、他のメンバーのスキルを確認

最近の案件は、1アプリを2~3人くらいで新規開発をする案件がそこそこあるイメージです。
ただし、複数人開発と言えど、

  • 新人さん(2年未満)
  • アプリを一人で作ったことのない人

が相方として参画しそうな場合は、申し訳ないですが確実に避けるようにしています。
(他の人の面倒を見るのは避けたい。ただし、面倒を見る費用をもらえる場合は別途相談)

また、開発規模の割に人数が多い案件の場合は、
スキルの低い人間を集めて、クライアントへの見積もりをカサ増ししている案件が多いです。
(できる人が1人で2ヶ月できるのを、4人で半年とか)
結局、できる人間が全て作るハメになるので、避けるようにしています。

SIer系は避ける

会社さんによりますが、古いルールとかに縛られていることがあるので、近づかないほうがいいイメージ。
特にスーツ必須のところは、ちょっと怖い。
※うちは、違うぜ!だ感じの会社さんはごめんなさい。経験上は、どうしても多いのです。

フロアのエンジニアの残業状況を確認

面談の際に常駐先のエンジニアの残業状態を確認します。
残業をしている人が多い場合は、だらだら仕事する癖がついている or 仕事量がおかしい 現場の可能性が高いので、なるべく避けます。

質問は、自分から質問すると残業を避けたがるエンジニアのイメージがつく気がするので、営業から質問してもらえるようにお願いするのがベストです。

スクラム開発の場合に、スクラムの前提を満たしているか確認

最近流行っているスクラム開発を取り入れている案件がそこそこ出てきました。
ただし、スクラムをやる上で、前提条件を満たしていない案件が多いので注意が必要です。

  • 全機能ができる納期がある????
  • スキルがバラバラすぎる
  • クライアントがスクラム開発に乗り気ではない(POさんがいないとか)
  • スクラムを銀の弾丸と思っている人がいる(スクラムだから開発に成功するとか)

(前提条件を満たしていない時点で、スクラムではないと思いますが。。。)

また、これは個人的な都合ですが、以下の点でスクラム開発はちょっとやりにくいところがあります。

  • 作業見積もりをごまかしにくい
  • 最初に一気に開発して、開発期間の最後の方でゆっくりすることがしにくい(開発期間の余ったところでだらだらするのが趣味のため)
  • 全員バラバラの所属で、ガッチリとしたチームプレーはちょっとめんどい

色々、ネガティブなことを書いてますが、うまくはまっているスクラム開発は色々と新しいことができて楽しいのでおすすめです!!

開発環境の確認

  • Mac/PCの簡単なスペック(メモリ8GBじゃ辛い)
  • iPhoneなどの実機の有無

などを念の為確認すると、あとで開発中にイライラするのを防げるかもしれないです。

まとめ

色々書きましたが、結局は現場に参画してみないとわかりません。

わざわざ、大変だったり辛い現場に行くのは勿体無いので、
お金をもらえながら、楽しくてスキルのつける現場になるべく行けるように、これからも頑張って面談していく予定です。

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

【SES】ドナドナされる時の面談時に地雷案件をなるべく避けるための確認ポイント7選(iOS/Androidアプリ開発)

私は、客先常駐でアプリを開発、保守するのをメインに5年ほどお仕事してきました。
iOSとAndroidのどちらかを担当して、機能やスケジュールの提案をしつつ、もくもくと開発していることが多いエンジニアです。

常駐のため、現場が変わるごとに面談があり(法律なんか知らん)、その度にやばそうな雰囲気の出てる案件は基本的に避けるようにしています。
現場に入る前の面談で、これを確認すれば、地雷案件を少し回避できるポイントがなんとなくわかってきたので、メモっておきます。
※あくまで個人的な指標であって、なんの根拠もないです!!

保守案件の場合は、アプリのストア評価を確認

保守案件で、ストアの評価が低いアプリは可能な限り参画を回避します。

理由は、

  • ストアの評価が低いものは、内部の品質が基本的に低い
  • 新たな開発はできずに、既存バグの修正に追われて疲弊する
  • ただのバグ修正ではスキルがつきにくい
  • クソコードを見ると発狂する(コントローラにAPIリクエストが入っているとか無理)
  • ひどい場合、gitすら使ってない
  • その他色々

だからです。
何より、過去に低評価アプリを3つほど、保守・機能追加を担当して、すごく苦労したので

アプリのアーキテクチャーを確認

  • 全てをControllerに実装するアーキテクチャ
  • MVC
  • MVVM
  • VIPER
  • CleanArchitecture
  • など

色々とありますが、開発するアプリのアーキテクチャが決まっている場合があるので、確認します。

保守開発の案件で、既存のアーキテクチャを教えていただけることがあるのですが、
なぜか「CleanArchitecture」を使用してると言われるプロジェクトは、クラス名だけCleanArchitectureで、中身はぐちゃぐちゃの地雷が多いです。

経験上、「CleanArchitecture」と「全てをControllerに実装するアーキテクチャ」は避けたい対象となっています。

常駐先の会社さんにアプリ開発をできるプロパーさんがいるか確認

受託開発の新規作成案件で、アプリを開発できるエンジニアさんがいない会社が受けた場合、要件定義や見積もりでやらかしている可能性がかなり上がります。
Web画面を作る感覚でアプリの見積もりをしてしまった案件の場合は、開発工数がそもそも足りていない場合が多くあるイメージです。

見積もりが多い分には、常駐する側としては問題ないのでが、やらかしている可能性が高い案件の場合が多いので、なるべく避けるのがおすすめです。

開発者を複数人募集している案件は、他のメンバーのスキルを確認

最近の案件は、1アプリを2~3人くらいで新規開発をする案件がそこそこあるイメージです。
ただし、複数人開発と言えど、

  • 新人さん(2年未満)
  • アプリを一人で作ったことのない人

が相方として参画しそうな場合は、申し訳ないですが確実に避けるようにしています。
(他の人の面倒を見るのは避けたい。ただし、面倒を見る費用をもらえる場合は別途相談)

また、開発規模の割に人数が多い案件の場合は、
スキルの低い人間を集めて、クライアントへの見積もりをカサ増ししている案件が多いです。
(できる人が1人で2ヶ月できるのを、4人で半年とか)
結局、できる人間が全て作るハメになるので、避けるようにしています。

SIer系は避ける

会社さんによりますが、古いルールとかに縛られていることがあるので、近づかないほうがいいイメージ。
特にスーツ必須のところは、ちょっと怖い。
※うちは、違うぜ!だ感じの会社さんはごめんなさい。経験上は、どうしても多いのです。

フロアのエンジニアの残業状況を確認

面談の際に常駐先のエンジニアの残業状態を確認します。
残業をしている人が多い場合は、だらだら仕事する癖がついている or 仕事量がおかしい 現場の可能性が高いので、なるべく避けます。

質問は、自分から質問すると残業を避けたがるエンジニアのイメージがつく気がするので、営業から質問してもらえるようにお願いするのがベストです。

スクラム開発の場合に、スクラムの前提を満たしているか確認

最近流行っているスクラム開発を取り入れている案件がそこそこ出てきました。
ただし、スクラムをやる上で、前提条件を満たしていない案件が多いので注意が必要です。

  • 全機能ができる納期がある????
  • スキルがバラバラすぎる
  • クライアントがスクラム開発に乗り気ではない(POさんがいないとか)
  • スクラムを銀の弾丸と思っている人がいる(スクラムだから開発に成功するとか)

(前提条件を満たしていない時点で、スクラムではないと思いますが。。。)

また、これは個人的な都合ですが、以下の点でスクラム開発はちょっとやりにくいところがあります。

  • 作業見積もりをごまかしにくい
  • 最初に一気に開発して、開発期間の最後の方でゆっくりすることがしにくい(開発期間の余ったところでだらだらするのが趣味のため)
  • 全員バラバラの所属で、ガッチリとしたチームプレーはちょっとめんどい

色々、ネガティブなことを書いてますが、うまくはまっているスクラム開発は色々と新しいことができて楽しいのでおすすめです!!

開発環境の確認

  • Mac/PCの簡単なスペック(メモリ8GBじゃ辛い)
  • iPhoneなどの実機の有無

などを念の為確認すると、あとで開発中にイライラするのを防げるかもしれないです。

まとめ

色々書きましたが、結局は現場に参画してみないとわかりません。
iOSアプリを開発すると聞いていて、Androidアプリを開発することはよくあるので。。。
スキルが複数ある場合は、しれっと他の案件にすり替わってることがあるので、回避不可能です。

わざわざ、大変だったり辛い現場に行くのは勿体無いので、
お金をもらえながら、楽しくてスキルのつける現場になるべく行けるように、これからも頑張って面談していく予定です。

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

Android Chrome/78.0.3904.108 にて起こっているpreload問題とその暫定的対応

何が起こっているのか

内容は上記の記事を見てもらうとすごくわかりやすくまとめてくれています。(同僚さんの記事です)
おそらく、ページ内の行動履歴?に関係する関連の高いaタグのhrefの中身を「自動で」preloadしてしまっているという問題

で、フロントとしてどうしたのか

まず、被害の大きい箇所、ページを調べて

a href="/hogehoge/bar"

span data-href="/hogehoge/bar"

という風に書き換えた上で、あとはJS側でdata-hrefの中身をclickでlocation.hrefに入れて飛ばす方法で暫定対応。
それか

a data-href="/hogehoge/bar"

という風にして、load時にdata-hrefの中身を抜き取ってhrefに入れるとかでも行けそうな。

雑感

ひとまず、被害の大きい箇所だけの暫定対応とはいえ、まだ対応ページがそこまでではなかったのでできる技かなと。
これでも完璧ではないので、追っかけ対応になることもありえる。
サイト規模の大きいところはその限りではないだろうから大変かと思います。

早くchrome側のアップデートなりで修正されることを望むばかり

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