Kotlin 日付の計算

都度ググってしまうのでここにメモしておく。

日時の加算、減算

Calendarクラスを使用する。

この例では7日前をDateオブジェクトで受け取る。run{}を使うとすっきり書ける。

val prev_week:Date = Calendar.getInstance().run {
    add(Calendar.DATE, -7 )  //7日前を求める
    time  //getTime()
}
日付文字列をDateオブジェクトに変換
if( SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").parse("2014-07-16T14:30:00.000+09:00") < prev_week ){
    //doAction
}


Kotlin Listの中の特定の値を持ったオブジェクトをfilterする

多分、みんなListの中にオブジェクトをぶち込んで利用することが多いと思う。

Listの中の特定のオブジェクトをいくつかfilterしたい場合とかどうやってるのかなあ...と思った。

こんな感じで書いたんだけど果たして... この場合はmuteListの中に含まれるオブジェクトを除外する。

fullList.filter{ object ->
    var ret = true
    muteList.forEach{ muteObject ->
        if( muteObject.name == object.name ){
            ret = false
            return@forEach
        }
    }
    ret
}

anyを使えばもう少しすっきり書ける。

fullList.filter{ object ->
    !muteList.any{ muteObject ->
        muteObject.name == object.name
    }
}


Android ViewPagerで表示されたFragmentによって、ActionBar Itemの表示を切り替える

ViewPagerで表示されているFragmentによって、不要なActionBar Itemがある場合に非表示とする。


f:id:letitride:20190717121045p:plainf:id:letitride:20190717121049p:plain


Itemオブジェクトの保存

MenuItem生成時に表示制御を行いたいItemをメンバとして記録しておく。lateinitで宣言すると、ViewPagerに渡した時にまだ初期化されていないので、Optionalで宣言する。

var icon:MenuItem? = null

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    getMenuInflater().inflate(R.menu.main_item, menu);
    icon = menu!!.findItem(R.id.icon_item)
    return super.onCreateOptionsMenu(menu)
}


ViewPager.addOnPageChangeListener

ViewPagerの切り替えイベントはaddOnPageChangeListenerに定義できる。このイベント定義で非表示にしたいFragmentが前面に来た場合、disableにすればよい。

viewPager.addOnPageChangeListener(object :ViewPager.OnPageChangeListener{

    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
        //icon初期化済?
        icon?.let {
            //iconの表示初期化
            it.setVisible(true)
            //表示されているFragmentの位置
            val i = viewPager.currentItem
            //2つめのFragmentの表示時はiconを非表示
            if(i == 1){
                it.setVisible(false)
            }
        }
    }
    // must override methods
    override fun onPageScrollStateChanged(state: Int) {}
    override fun onPageSelected(position: Int) {}


Android Fragmentの中にSupportMapFragmentを配置する。

例えばViewPagerに配置したFragmentに地図を配置したい場合などなど。

そのままFragmentのレイアウトにベタっとSupportMapFragmentを配置して、

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ParentFragment">

    <fragment
        android:id="@+id/map"
        android:name="com.google.android.gms.maps.SupportMapFragment"
        ...
/>
</androidx.constraintlayout.widget.ConstraintLayout>

親Fragment側から、findFragmentByIdすれば取得できるかなーなんて思ってたけど、ダメだった。

fragmentManager?.findFragmentById(R.id.map)  //return null

実装例

FragmentTransactionからreplaceするやり方で配置できる

layout
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ParentFragment">

        <FrameLayout
            android:id="@+id/map"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
ParentFragment
lateinit var mMap:GoogleMap

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    val fragmentTransaction = fragmentManager?.beginTransaction()
    fragmentTransaction?.let{
        val mapFragment = SupportMapFragment()
        mapFragment.getMapAsync(this)
        fragmentTransaction.replace(R.id.map, mapFragment);
        fragmentTransaction.commit();
    }
    super.onViewCreated(view, savedInstanceState)
}

override fun onMapReady(googleMap: GoogleMap) {
    mMap = googleMap
}


Android MapViewを利用する

APIキーの取得

MapViewを利用するには予めAndroid用GoogleMapのAPIキーを取得しておく必要がある。

Google Cloud PlatformでAPIキーの取得を行っておく。

プロジェクトを作成後、APIとサービスを有効化からMaps SDK for Androidを選択すれば良い。


プロジェクトの作成 プルダウンを押下すると新規作成の項目がある。

f:id:letitride:20190716100232p:plain

APIとサービスの有効化 右側の+アイコンより選択画面に進める。

f:id:letitride:20190716100323p:plain

認証情報の欄からアクセス用のAPIキーを作成、確認できる。

f:id:letitride:20190716100532p:plain

app/build.gradle

google play serviceを使用する

implementation 'com.google.android.gms:play-services-maps:17.0.0'
Manifest

Manifestに取得したAPIキーを記述する。通常は専用のresourceファイルなどで記述し、.ignore指定すればよい。

<meta-data
   android:name="com.google.android.geo.API_KEY"
   android:value="ここに取得したAPIキーを記載"/>
layout

MapViewオブジェクトをそのまま使用せず、SupportMapFragmentを配置するようにする。

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          xmlns:map="http://schemas.android.com/apk/res-auto"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:id="@+id/map"
          tools:context=".MapsActivity"
          android:name="com.google.android.gms.maps.SupportMapFragment"/>
Activity

配置したSupportMapFragmentにOnMapReadyCallbackの実装を渡せばよい。

class MapsActivity : AppCompatActivity(), OnMapReadyCallback {

    private lateinit var mMap: GoogleMap

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

        val mapFragment = supportFragmentManager
            .findFragmentById(R.id.map) as SupportMapFragment
        mapFragment.getMapAsync(this)
    }

    override fun onMapReady(googleMap: GoogleMap) {
        mMap = googleMap
    }
}
カメラの移動

地図の初期位置や表示位置の変更等に使用する。

fun cameraPosition(mMap:GoogleMap){
    var defaultPosition = LatLng(35.70316439, 139.57984714)
    //カメラオブジェクト zoomレベルは小さいほど広角になっていく。
    val camera = CameraUpdateFactory.newLatLngZoom(defaultPosition, 15f)
    //カメラの移動
    mMap.moveCamera(camera)
}
ピンマーカーの設置
fun setMarker(mMap:GoogleMap){
    val options = MarkerOptions()
    options.position( LatLng(35.70436915, 139.57947137) )
    mMap.addMarker(options);
    options.position( LatLng(35.70138951, 139.5772505) )
    mMap.addMarker(options);
}
ピンマーカーのクリックイベントリスナー

選択されたmarker.idは"m"addMarkerされた順序のidentifierとなっている。 returnするbooleanはクリック時にカメラを移動するか否か。「しない」がtrueなので注意。

mMap.setOnMarkerClickListener { marker -> 
    val id = marker.id.replace("m", "").toInt()
    Toast.makeText(this, id.toString(), Toast.LENGTH_SHORT).show()
    //click時にカメラを移動する trueは移動しない
    false
}
ピンマーカーにセットしたタイトルのクリックイベントリスナー

setOnInfoWindowClickListenerを定義する

mMap.setOnInfoWindowClickListener { marker ->
    val id = marker.id.replace("m", "").toInt()
    Toast.makeText(this, id.toString(), Toast.LENGTH_SHORT).show()
}

サンプルコード

github.com

MapViewのサンプルアプリはこの本でも触れられている


俺のイカれたアプリメンバー達を紹介するぜえ〜 個人開発で毎日1アプリをリリースして1週間経った感想

まずはFFIVタイムズ!

play.google.com

これは作者自身もプレイしてるオンラインRPG、FF14というゲームのアンテナアプリだ。

自身もiPhoneのFF14のアンテナアプリ使ったりするんだけど、広告が邪魔だったり、サムネが重かったりしてギガ食いそうだから不満部分を自分なりに改善したものを作った。サムネに関してはやっぱり重くなった部分もあるけど...

2ch系のまとめ以外にもプレイヤーのブログのRSSも取得しているのがこだわりの一つ。こういうゲームってプレイヤーの繋がりが強いからいいアイデアだと思ったのだ。

かなりのコアなファンがいるゲームだし、大規模な拡張に合わせてリリースしたので結構いけるんじゃないかと思ったんだけど、初動は全然だめ。インストールされてすぐにアンインストールされたことも。

今は少ないながらも徐々にリピーターがつき始め、何故か台湾の方がリピーターになってたりする。

本音をいうとappleのデベロッパープログラムにも入ってるし、このアプリに限らず全てのアプリのiOS版も作りたかったんだけど、最近、この手のアプリはappleの審査がきつそうでAndroidのみに照準を絞った。

切り込み隊長のDQXタイムズ

play.google.com

まあ、所謂ガワネイティブに近い(いや、RecyclerViewとかSQLiteとか使ってるよ)んだけど、FFIVの中身がDQXに差し変えただけなのだ。

DQ10は配信するブログを調査してる時に、一番、プレイヤーのブログが盛り上がってたように感じた。みんなすごく丁寧に可愛くブログを運営されていて、これはいける!と思ったのだ。

これも初動は全然だめ。というかASO的になぜかドラクエとかで全然引っかからなくて箸にも棒にもかからない。

けどけど、このアプリがリピーターを掴んだ第1号のアプリなのだ。

エースのモンハンタイムズ

play.google.com

FFIVの中身がモンハンに(略

このアプリが初動で一番伸びた。といってもまあ、一桁のDL数なんだけど。やっぱり蓋を開けてみたらFF14、DQ10、モンハンの3つがリピーター、滞在時間が長い。

いぶし銀のFPS/TPSまとめ速報

play.google.com

FFIVの中身が(略

FPS/TPSはyoutubeの中では結構盛り上がっていて、チャンネル登録が多いジャンルの一つだと思っている。作者自身、FPSプレイヤーなので、よくまとめサイトとか見ている。類似のアンテナアプリがないようなので、イケるんちゃうの?と思って作ってみた。

結果はダウンロード数は最下位。だけど、広告収益がはじめて出たアプリでいぶし銀なのだ。

伏兵のゲームニュース速報

play.google.com

FFIVの中身が(略

初動はモンハンの次に伸びた。意外な伏兵。リピート率もそこそこ。2番打者あたりを任せたい。

副業・アフィリエイト・働き方ニュースまとめ

play.google.com

これは作者が会社員時代に少しだけプログラミングが出来るし、副業OKの会社だったから、何かマネタイズ出来ないかな〜とか思ってた時に読んだりしていたブログ群をまとめたアプリだ。

ちょっと口が悪くなるが、扱ってるブログは「読ませるためのブログ」な論調が多いので、このブログ群に書いていることを信じたり投資したり、はたまた会社を辞めたりするのは個人の判断だ。あくまでお金を生み出す思考の一種やどう働くか?として捉えると勉強になることもある。個人的には会社務めの人は「自由な働き方」という言葉には慎重に判断して欲しい。

今の時代、ニーズがあるワードだと思ったんだけど、ダウンロードは全然ダメ。

連続リリースから1週間経過しての感想

最後のアプリをリリースしてから1週間が経過した。なので最初にリリースしたアプリからは2週間ほど経過したことになる。

結果は6つ合計で28DL。うち、6つは作者自身のダウンロードなので22DL。まあ、1つあたりの平均だとショボいけど、6つ合計だけど、多少は見れる数字になったのかな笑

1つのアプリを作るのは掲載するブログの調査含めて半日ほどなので、この手のアプリだとジャンルごとにバラ撒くのは一つの手法だと思うし、実際やってるデベロッパーもちらほらいるね。

やっぱりこの手の更新頻度が高いコンテンツアプリはリピートと滞在時間が長いのだ。徐々に延べDLが増えていくのが楽しみなのだ。

僕の理論はこれ。

f:id:letitride:20190714080018j:plain:h300

みんなが気になる広告クリック率とクリック単価

広告はアプリ再下段のバナーのみ配置。

CTRは0.5%切るくらい。単価は思っていたよりは高い。といった感じ。100円は超えない。

収益は?

リリース一週間、22DLで出るわけがない。

先週はリリースお休みしたので、今週はまた2つ3つくらい出したいと思います!

最新バージョンのライブラリがAndroidXのサポートライブラリに依存する

現在の導入手順に従って、最新のfirebase-coreのライブラリ

implementation 'com.google.firebase:firebase-core:17.0.0'

を入れると、利用しているSupport Library次第ではAndroidXのSupport Libraryが必要と叱られます。

manifest merger failed : attribute application@appcomponentfactory value=(android.support.v4.app.corecomponentfactory) from [com.android.support:support-compat:28.0.0] androidmanifest.xml:22:18-91

firebaseのライブラリに限らず、AndroidXのSupport Libraryに依存するライブラリバージョンが出始めていて、これからはAndroidXのSupport Libraryがメンテ対象となるようです。特別な理由がない限り利用するSupport LibraryをAndroidXに入れ替えたほうがいいと思います。

私の環境では以下の操作1つで簡単に入れ替えが出来ました。途中でZipのバックアップを取るか?と質問があるので、チェックをつけてバックアップを作成しておきましょう。

f:id:letitride:20190701112340p:plain:h400

また、理由があってAndroidXのSupport Libraryを導入できない場合は依存しているライブラリのバージョンを下げるとビルド出来る場合があります。

firebase-coreの場合、敢えて17.0.0を使わずに16.0.8を使用するとビルド可能となる場合があります。

implementation 'com.google.firebase:firebase-core:16.0.8'