何者にもなれなかった自分について

退職して1年が経った。

在職中、あれだけ自由にプログラミングできる時間に恋焦がれていたのだが、いざ時間が出来てしまうとダラダラしてしまう。 本当に自分はダメな奴だなと思う。

知人のスタートアップのシステム作成のお手伝いと少しのiOSアプリを公開しただけだった。

世の中には仕事をしながら時間が欲しくて欲しくて堪らなく、少しの時間を個人の勉強や開発に充てている人もいる中で、自分は無駄に時間を浪費しているだけだ。

ネット上でキラキラと情報発信しているエンジニア達のように自分も何かモノを作りつづけて発信できるかのように感じていた。

自分はプログラミングが好きだと思っていた。仕事ではなく自分がしたいことだけのプログラミングだと1日中でも書いてられると思っていたが、そうではなかったらしい。

作りたいもののアイデアはあってもリリースまで集中力が続かない。飽きっぽいというか継続するという力が絶対的に足りないのだろう。会社員時代は品質に満足いくものではなかったものの、ある程度の規模のシステムをローンチしていた。

といっても、会社員に戻ろうという気も今はあまりない。こういうところも自分のダメなところだ。

会社員時代、エンジニアとして結果が残せなかったのは人のせいだと思っている。自分に能力がなかったことも認めるが人のせいにすることで「運が悪かった。」や、「俺は悪くない。よくやった。」などと自身を正当化して生きている。ドブさらいのように攫っても攫ってもゴミを攫い続けていたつもりだ。そして当時、心が壊れた。

20歳の頃、自分は何者かになる人間なんだろうと勝手に思っていた。金もない、人付き合いも苦手、親のスネをかじっていた自分が到底努力とはいえない努力をし、世の中の不条理や純朴な人間が成功するような美談を説けば、何者かになれると思っていた。思えばあの頃が一番充実した時間だったのかもしれない。

ただ、それでも、こんな自分でも変わったことがあった。当時あれだけ憧れた何者かになることを諦めていたことだ。

退職後すぐにあの頃住んでいた町に訪れた。変わったところ、当時と変わらないままのところ。

この町が何者にもなれなかった自分の原点だったんだろう。

#「迷い」と「決断」

りっすん×はてなブログ特別お題キャンペーン〜りっすんブログコンテスト2019「迷い」と「決断」〜 Sponsored by イーアイデム

Android Kotlin ListViewの次のN件の読み込み

よくあるListViewの最終セルに次のN件の読み込み的なやつ。ページネーションと読んでいいのかどうか。

Activity

class MainActivity : AppCompatActivity() {

    lateinit var myListView:ListView
    lateinit var myAdapter:MyAdapter

    val listItems = ArrayList<String>()
    var columnIndex = 0

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

        //listItemの追加
        addItem()
        myListView = findViewById<ListView>(R.id.postsListView)
        myAdapter = MyAdapter(this, listItems)
        myListView.adapter = myAdapter
    }

    private fun addItem(){
        listItems.add( columnIndex.toString() )
        columnIndex++
    }

    public fun loadNextItem(){
        addItem()
        //itemの変更をadapterに通知
        myAdapter.notifyDataSetChanged()
    }
}

Adapter

ViewHolder
private class ViewHolder(view: View) {
    val name = view.findViewById<TextView>(R.id.name)
}
各セルになるViewの作成メソッド

itemセルはlist_item_cell.xml、次へのセルはlist_next_cell.xmlにlayout定義されているものとする。

次へのセルとなるViewにもタグをつけておくとgetView()時に再利用されるconvertViewの識別に利用できる。

mainActivity.loadNextItem()notifyDataSetChanged()this.notifyDataSetChanged()としてもこのケースでは動作するが、ほとんどのケースでは別スレッドから取得したデータをUIスレッドに渡すと思うので、Activity側でadapterに通知するほうがハマりどころが少ない。

class MyAdapter(
    val context:Context,
    val items: ArrayList<String>
) :BaseAdapter() {

    private val inFlater = LayoutInflater.from(context)

    private fun makeNextView(parent: ViewGroup?):View{
        val view = inFlater.inflate(R.layout.list_next_cell, parent, false)
        view.tag = "nextView"
        view.setOnClickListener {
            val mainActivity = context as MainActivity
            mainActivity.loadNextItem()
        }
        return view
    }

    private fun createView(parent: ViewGroup?):View{
        val view = inFlater.inflate(R.layout.list_item_cell, parent, false)
        view.tag = ViewHolder(view)
        return view
    }
}
getCount()

getCount()は次へセルの分を足してreturnする

override fun getCount(): Int {
    return items.size + 1
}
getView()

positionがitems配列の終端に達した時はページネーションセルを返す。

またconvertViewが次へになるセルのリサイクルになる時がある為、セットされたタグで再利用可能であるViewか確認を行う。

override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {

    //ページネーションセル
    if(items.size <= position){
        return makeNextView(parent)
    }
    //コンテンツセル
    var view = convertView ?: createView(parent)
    if(view.tag == "nextView"){
        view = createView(parent)
    }
    val viewHolder = view.tag as ViewHolder
    viewHolder.name.text = items[position].title
    return view
}

MyAdapter

上記を記述したAdapterクラス

class MyAdapter(
    val context:Context,
    val items: ArrayList<String>
) :BaseAdapter() {

    private val inFlater = LayoutInflater.from(context)

    private class ViewHolder(view: View) {
        val name = view.findViewById<TextView>(R.id.name)
    }

    private fun makeNextView(parent: ViewGroup?):View{
        val view = inFlater.inflate(R.layout.list_next_cell, parent, false)
        view.tag = "nextView"
        view.setOnClickListener {
            val mainActivity = context as MainActivity
            mainActivity.loadNextItem()
        }
        return view
    }

    private fun createView(parent: ViewGroup?):View{
        val view = inFlater.inflate(R.layout.list_item_cell, parent, false)
        view.tag = ViewHolder(view)
        return view
    }

    override fun getCount(): Int {
        return items.size + 1
    }


    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {

        //ページネーションセル
        if(items.size <= position){
            return makeNextView(parent)
        }
        //コンテンツセル
        var view = convertView ?: createView(parent)
        if(view.tag == "nextView"){
            view = createView(parent)
        }
        val viewHolder = view.tag as ViewHolder
        viewHolder.name.text = items[position].title
        return view
    }
}

実際にIDEで記述して実行したわけではないので、ところどころTypoだったり、足りないところがあるかもしれません。

Kotlin bitmapの扱い

ByteからBitmap

public fun ByteToBitmap(bytes:ByteArray):Bitmap{
    val opt = BitmapFactory.Options()
    opt.inJustDecodeBounds = false
    return BitmapFactory.decodeByteArray(bytes, 0, bytes.size, opt)
}

//ImageViewに画像をセット
ImageView.setImageBitmap( ByteToBitmap(byteArray) )

BitmapからByte

public fun BitmapToByte(bmp:Bitmap):ByteArray{
    val stream = ByteArrayOutputStream()
    bmp.compress(Bitmap.CompressFormat.PNG, 100, stream)
    return stream.toByteArray()
}

streamからBitmap

var inputStream = activity!!.contentResolver.openInputStream(uri)

public fun StreamToBitmap(inputStream:InputStream):Bitmap{
    val opt = BitmapFactory.Options()
    opt.inJustDecodeBounds = false
    BitmapFactory.decodeStream(inputStream, null, opt)
    inputStream.close()
    return bmp
}

Android 共有ストレージアクセス

アクセス権限の確認 & 取得

ダイアログで権限の許可 or 拒否の操作完了時にonRequestPermissionsResult()がコールバックされる。

fun accessStorage(){
    if(ContextCompat.checkSelfPermission(
        activity!!, Manifest.permission.READ_EXTERNAL_STORAGE)
            == PackageManager.PERMISSION_GRANTED )
    {
        //権限あり
        readExternalStorage()
        return
    }
    //権限なし
    if(shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE)){
        //拒否済み
    }
    //権限許可確認ダイアログ
    requestPermissions( arrayOf<String>(Manifest.permission.READ_EXTERNAL_STORAGE), 2 )
}

共有ストレージのアクセス

暗黙インテントを送信する。ストレージから戻ってきた時にonActivityResult()がコールバックされる

private fun readExternalStorage() {
    val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
    intent.setType("image/*")
    startActivityForResult(Intent.createChooser(intent,"Title"), 1)
}

権限要求結果

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    if(requestCode != 2){return}
    if(grantResults.size != 1 || grantResults[0] != PackageManager.PERMISSION_GRANTED){
        //要求拒否
        return
    }
    //ストレージアクセス
    readExternalStorage()
}

ストレージから戻ってきた時の処理

この例では保存されている画像を選択された時を想定 data:Intent?の中に選択されたファイル情報が入ってくるので、ファイルに対する処理を記述する

 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
     super.onActivityResult(requestCode, resultCode, data)

     if(requestCode != 1 ) {return}
     if(resultCode != RESULT_OK){return}
     //選択なし
     if(data == null){return}

     data.data?.let { uri ->
         //選択された画像に対する処理
     }

Android Kotlin ActivityからFragmentを追加

supportFragmentManagerでlayoutにfragmentを割り当てる

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

        val fragment = MyFragment()
        val t = supportFragmentManager.beginTransaction()
        t.add(R.id.layout_id, fragment)
        t.commit()
    }
}

Android AppWidgetProviderのインスタンス調査

widget間で値共有できるか調査してみたのでメモ

Widgetごと、ライフサイクルごとにインスタンスが生成されているみたい。なので、いくらこういうことをしても

class MyWidget : AppWidgetProvider() {

    private var numberCount = 0

    override fun onUpdate(context: Context,
        appWidgetManager: AppWidgetManager, appWidgetIds: IntArray)
    {
        Log.d("lifecycle", "Update")
        Log.d("lifecycle", this.toString())
        numberCount += 1
        for (appWidgetId in appWidgetIds) {
            updateAppWidget(
                context, appWidgetManager, 
                appWidgetId, numberCount.toString()
            )
        }

    override fun onEnabled(context: Context) {
        Log.d("lifecycle", "Enable")
        numberCount += 1
        Log.d("lifecycle", this.toString())
    }

    override fun onDisabled(context: Context) {
        Log.d("lifecycle", "Disable")
        Log.d("lifecycle", this.toString())
    }

    companion object {

        internal fun updateAppWidget(
            context: Context, appWidgetManager: AppWidgetManager,
            appWidgetId: Int, numberCount: String
        ) {

            // Construct the RemoteViews object
            val views = RemoteViews(context.packageName, R.layout.my_widget)
            views.setTextViewText(R.id.appwidget_text, numberCount)

            // Instruct the widget manager to update the widget
            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }
}

すべてのWidgetで"1"となる。onEnableが着火したWidgetも"1"なので注意。

logはこんな感じ。インスタンスIDが各lifecycleごとに違うのがわかる

2019-05-30 19:12:06.960 12210-12210/jp.letitride.mystudywidget D/lifecycle: Enable
2019-05-30 19:12:06.960 12210-12210/jp.letitride.mystudywidget D/lifecycle: jp.letitride.mystudywidget.MyWidget@a438aaa
2019-05-30 19:12:06.963 12210-12210/jp.letitride.mystudywidget D/lifecycle: update
2019-05-30 19:12:06.963 12210-12210/jp.letitride.mystudywidget D/lifecycle: jp.letitride.mystudywidget.MyWidget@c7e069b
2019-05-30 19:12:45.655 12210-12210/jp.letitride.mystudywidget D/lifecycle: Disable
2019-05-30 19:12:45.655 12210-12210/jp.letitride.mystudywidget D/lifecycle: jp.letitride.mystudywidget.MyWidget@79c0a11

よって、onEnabledで別ライフサイクルや他Widgetに状態を共有するには、companion object {}内でメンバを扱ったり

override fun onEnabled(context: Context) {
    numberCount += 1
}

companion object {
    var numberCount = 0
    // ... 中略
}

または、onReceive()intent.actionでイベントハンドルしつつintentのExtraで情報の受け渡しするのが良さそう。

この画像は同Widgetを複数配置した時のcompanion object内に記述したstaticフィールドでの値参照。

f:id:letitride:20190530194959p:plain

Android Kotlin SoundPool()のdeprecated対応

SoundPool() はAPI LEVEL 21 から非推奨。

'constructor SoundPool(Int, Int, Int)' is deprecated. Deprecated in Java

21以降はSoundPool.Builder()でインスタンスを構築する。

//Lollipop未満
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP){
    mSoundPool = SoundPool(1, AudioManager.STREAM_ALARM, 0)
}else{
    //AudioAttributesでstreamTypeを指定
    val audioAttributes = AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_ALARM).build()
    mSoundPool = SoundPool.Builder()
        .setMaxStreams(1).setAudioAttributes(audioAttributes).build()
}
mSoundResId = mSoundPool.load(this, R.raw.soundfile,1)
mSoundPool.play(mSoundResId, 1.0f, 1.0f, 0,0, 1.0f)