My Note Pad

エンジニアリングや日々の雑感を書いていきます

OPPO Reno Aを購入したので開封!

OPPO Reno A購入の背景

楽天モバイルの無料サポータープログラムに当選したためです。 無料サポータープログラムでは対象の機種を購入する必要があり、今回はOPPO Reno Aを購入したので開封していきます。

当選のメールが到着してから、申込みのメールがなかなかこなかったのでソワソワしていましたが、OPPO Reno Aの在庫があるタイミングだったのでそのまま購入しました。
※ブルーが良かったなと思いつつ、ブルーは在庫がなかったのでブラックを購入しています。

実際に注文してからは3日後に届きました。

開封

早速開封していきます。
ダンボールの中身は、OPPO Reno Aの箱と、SIMカード、スタートガイドでした。
写真だとわかりにくいですが、箱のサイズはかなり大きいです。

f:id:yuki10k:20191020214621j:plain

iPhone 11と比較してみると、かなり大きいことが分かります。

f:id:yuki10k:20191020214631j:plain

早速開封していきます。
蓋を開けた直後は、本体とはご対面できず・・

f:id:yuki10k:20191020214643j:plain

説明書の下に本体が入っていました。
ぱっとみこれは裏面か?と思いましたが、フロントパネル側でした。

f:id:yuki10k:20191020214654j:plain

さらにその下には透明なケースと、USB-Type Cのケーブル。
このケースは柔らかい素材のものでした。

f:id:yuki10k:20191020214706j:plain

ケースの下に充電器と、イヤホンが付属しています。
OPPO Reno Aには最近のスマホには珍しくイヤホンジャックがあります。

f:id:yuki10k:20191020214717j:plain

iPhone11との画面サイズ比較です。
OPPO Reno Aの方が若干ですがサイズが大きいことが分かります。

f:id:yuki10k:20191020214739j:plain

数日使ってみての感想

メインで使っている端末はPixel3なので、そちらとの比較になります。

電波状況

※10月に開始された楽天モバイルのMNO回線です

  • 家で使っていると窓から離れると圏外になってしまう
  • 都心にあるオフィスでは基本的には繋がる
  • 地下鉄ではau回線に繋がるため快適
  • 利用者がまだ少ないからか、MVNO回線に比べると昼や夜は繋がりさえすれば快適

OPPO Reno A

  • Pixel3と比較すると、画面がかなり大きく動画やKindleを見るのも快適
    • Pixel3では漫画を読む場合はたまに拡大が必要でした。
  • バッテリー持ちは良好
    • Pixel3と比べると安心感がかなりあります。 ※Pixel3のバッテリー持ちが良くないからというのもありますが・・
  • ゲームをするには若干スペック不足
    • ドラゴンクエストウォークをプレイしてみた感じでは、標準画質ではモタつきが発生しストレスフルでした。
    • プレイを再開しようとしても起動画面からになる (メモリは6GBあるのになぜ・・・
  • Pixel3に比べてモタつきが発生する

といった感じです。
基本的にはゲームをプレイしなければ普段使いには全問題ない印象でした。

また、若干のもたつきに関しては、以下の 高パフォーマンスモード に設定することでかなりマシになりました。
バッテリーの持ちがどうなるか若干気がかりですが、それでもPixel3よりは持つ印象です。

f:id:yuki10k:20191023130138p:plain
高パフォーマンスモード

まとめ

  • 楽天の回線はまだまだこれからという印象で通常のサービス開始までに良くなってくれればいいなと思います。
  • OPPO Reno Aは普段使いでは全く問題なし。
  • コスパ良すぎる。
  • Felicaは試していないですが、おそらく問題なく使えると思います。
  • 今までハイエンドな端末を使っていた人は、違和感を感じると思います。
  • 今後はメインはPixel3を引き続き使いつつ、OPPO Reno A + 楽天回線も様子を見ていきたいと思います。

Pixel3 aが発売されたこのタイミングでPixel3を購入・・!

タイトルの通りですが、Pixel3aが発表されたにもかかわらず、Pixel3を購入しました。

store.google.com

前々からPixel3は気になっていましたが、以下の記事を見て雑誌を購入して、

japanese.engadget.com

「Pixel3すごい(語彙力・・)」
「このカメラ欲しい」

という感じで、ずっと悶々としていました。 しかし、定価9万5千円はちょっと高いのでバレンタインセールのようにディスカウントが発生するタイミングを虎視眈々と狙っていました。

Googleストアで「Pixel 3」のNot Pinkが2万円オフのセール、Pixel 3 XLなら2万5000円オフ - Engadget 日本版

全く安くなる気配は無く、月日が流れ。。

Pixel3aという廉価版が出るらしいというので、常に情報をウォッチしていたところ、 5月7日のGoogle I/Oでついに発表されました。(眠い目を擦りながらリアルタイムで視聴

store.google.com

値段も5万円以下ということで、速攻でポチり、17日発売を待つのみという状態に。
その後もソワソワしながら色々調べていると、Pixel3aには「Pixel VisualCore」が載っていないという情報が。

カメラに関しては素人なため、正直気にならないだろうと思っていましたが、 ラクマを眺めていたところ、

が58,000円で販売されていました。

ラクマで3,000円引きのクーポンを配布していたため、55,000円で購入できることに。

Pixel3aの定価が48,600円なので、Pixel3a + 6,000円ほどでPixel3のSIMフリーが手に入ることになります。しかもUSB Type-Cのイヤホン付き。

個人間取引ということで赤ロムやSIMロック未解除といった不安がありましたが、 IMEI番号やSIMロック解除画面の画像も載せてくれていたので信用して購入し、Pixel3aはキャンセルしました。
※本記事を参考にフリマアプリなどで購入し、不利益を被られても一切の責任を負いかねます。

購入したPixel3は当日中に発送していただき、翌日には到着。

早速開封していきます。

丁寧にプチプチで梱包されていました。 f:id:yuki10k:20190520142911j:plain

Pixel3の箱が登場
角が少し潰れていましたが特に問題なし。

f:id:yuki10k:20190520142916j:plain

蓋を開けると、フィルムに包まれたままのPixe3が登場。

f:id:yuki10k:20190520142920j:plain

イヤホンや充電ケーブルも未開封のまま。

f:id:yuki10k:20190520143006j:plain

f:id:yuki10k:20190520143013j:plain

SIMもアンロックされており、IIJmiodocomo SIMでも問題なく通信することができました。

早速色々撮影していきます。
まずは東京駅

気持ちキレイに撮れている気がします。

f:id:yuki10k:20190520143053j:plain

夜バージョン。
近未来感があります。
Night Sightは未使用です

f:id:yuki10k:20190520143056j:plain

続いて食べ物系。
ラーメンや

f:id:yuki10k:20190520143051j:plain

ハンバーガ

f:id:yuki10k:20190520143057j:plain

お肉もお手のもの

f:id:yuki10k:20190520143111j:plain

鮮やかな肉の塊

f:id:yuki10k:20190520143100j:plain

まだこれくらいしかPixel3で写真の撮影をできていませんが、現時点では非常に満足です。
iPhone Xで撮影した写真と比較しても鮮やかに撮れるなーという印象です。

とはいえiPhoneのエコシステムからはなかなか抜け出せないので、しばらくはiPhone Xをメイン端末として引き続き使用しつつ、Pixel3はカメラ中心で0simで運用していこうと思います。

CodelabsのAndroid Room with a ViewをKotlinでやってみた

Codelabsについて

Googleが提供している、様々なチュートリアルやハンズオンのコースです。 https://codelabs.developers.google.com/

Android Room with a View

AndroidRoom ViewModel LiveDataについて学ぶことができるコースです。

https://codelabs.developers.google.com/codelabs/android-room-with-a-view/index.html?index=..%2F..%2Findex#0

GoogleがKotlinで実装した良いサンプルとして、こちらも公開されています。 https://github.com/googlesamples/android-sunflower

motivation

Android開発の学習をする上で、LiveData について押さえておきたかったので、Room with a Viewのcodelabsを見つけました。 手を動かしながら学べるのでちょうど良いと思いましたが、実装がJavaだったので置き換えつつ進めてみることにしました。

ゴールはCodelabのアプリがKotlinコードで動作することです。 Javaの元コードを見て、Kotlinだとこんな感じかな〜という風に書き換えて実装していったものになるので、正しい書き方とは異なる可能性もあります。

Environment

各セクション毎の変更点

2. Create your app

特になし。 Kotlinでアプリを作成します。

3. Update gradle files

kotlin-kaptプラグインを有効にし、annotationProcessorの定義をkaptに変更しています。

apply plugin: 'kotlin-kapt'
・
・
・
    // Room components
    implementation "android.arch.persistence.room:runtime:$rootProject.roomVersion"
//    annotationProcessor "android.arch.persistence.room:compiler:$rootProject.roomVersion"
    kapt "android.arch.persistence.room:compiler:$rootProject.roomVersion"
    androidTestImplementation "android.arch.persistence.room:testing:$rootProject.roomVersion"

    // Lifecycle components
    implementation "android.arch.lifecycle:extensions:$rootProject.archLifecycleVersion"
//    annotationProcessor "android.arch.lifecycle:compiler:$rootProject.archLifecycleVersion"
    kapt "android.arch.lifecycle:compiler:$rootProject.archLifecycleVersion"

4. Create the entity

Entityの定義方法自体がJavaとKotlinで大きく異なり、かなり楽に記述できます。

import android.arch.persistence.room.ColumnInfo
import android.arch.persistence.room.Entity
import android.arch.persistence.room.PrimaryKey

@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") var mWord: String)

5. Create the DAO / 6. The LiveData class

  1. ではgetAllWords() の戻り値はList<Word>ですが、6.でLiveData<List<Word>>に修正する
import android.arch.lifecycle.LiveData
import android.arch.persistence.room.Dao
import android.arch.persistence.room.Insert
import android.arch.persistence.room.Query
import com.example.hoge.roomwithaview.entity.Word

@Dao
interface WordDao {
    @Insert
    fun insert(word: Word)

    @Query("DELETE from word_table")
    fun deleteAll()

    @Query("SELECT * from word_table ORDER BY word ASC")
    fun getAllWords(): LiveData<List<Word>>
}

7. Add a Room database / 12. Populate the database

getDatabaseメソッドはcompanion objectで定義していますが、package-level functionで定義するのが正しいかも。 ※sunflowerサンプルアプリケーションでも実装はcompanion objectで実装されていました。

PopulateDbAsyncクラスは12. Populate the databaseでの実装になります。 ここはCallbackの書き方で少しハマりました。

import android.arch.persistence.db.SupportSQLiteDatabase
import android.arch.persistence.room.Database
import android.arch.persistence.room.Room
import android.arch.persistence.room.RoomDatabase
import android.content.Context
import android.os.AsyncTask
import com.example.hoge.roomwithaview.dao.WordDao
import com.example.hoge.roomwithaview.entity.Word

@Database(entities = [Word::class], version = 1)
abstract class WordRoomDatabase: RoomDatabase() {

    abstract fun wordDao(): WordDao

    companion object {
        @Volatile
        private var instance: WordRoomDatabase? = null

        fun getDatabase(context: Context): WordRoomDatabase {
            if (instance == null) {
                synchronized(WordRoomDatabase::class.java) {
                    if (instance == null) {
                        instance = Room.databaseBuilder(context.applicationContext,
                                WordRoomDatabase::class.java, "word_database")
                                .addCallback(object : RoomDatabase.Callback() {
                                    override fun onOpen(db: SupportSQLiteDatabase) {
                                        PopulateDbAsync(instance!!).execute()
                                    }
                                })
                                .build()
                    }
                }
            }
            return instance!!
        }
    }
}

class PopulateDbAsync(db: WordRoomDatabase): AsyncTask<Void, Void, Void>() {
    private val mDao: WordDao = db.wordDao()

    override fun doInBackground(vararg params: Void?): Void? {
        mDao.deleteAll()
        var word = Word("Hello")
        mDao.insert(word)
        word = Word("World")
        mDao.insert(word)
        return null
    }
}  

8. Create the Repository

import android.app.Application
import android.arch.lifecycle.LiveData
import android.os.AsyncTask
import com.example.hoge.roomwithaview.dao.WordDao
import com.example.hoge.roomwithaview.db.WordRoomDatabase
import com.example.hoge.roomwithaview.entity.Word

class WordRepository(application: Application) {
    private val mWordDao: WordDao
    private var mAllWords: LiveData<List<Word>>

    init {
        val db = WordRoomDatabase.getDatabase(application)
        mWordDao = db.wordDao()
        mAllWords = mWordDao.getAllWords()
    }

    fun getAllWords(): LiveData<List<Word>> {
        return mAllWords
    }

    fun insert(word: Word) {
        InsertAsyncTask(mWordDao).execute(word)
    }
}

class InsertAsyncTask(wordDao: WordDao): AsyncTask<Word, Void, Void>() {
    private val mAsyncTaskDao: WordDao = wordDao
    override fun doInBackground(vararg params: Word?): Void? {
        mAsyncTaskDao.insert(params[0]!!)
        return null
    }
}

9. Create the ViewModel

import android.app.Application
import android.arch.lifecycle.AndroidViewModel
import android.arch.lifecycle.LiveData
import com.example.hoge.roomwithaview.entity.Word
import com.example.hoge.roomwithaview.repository.WordRepository

class WordViewModel(application: Application) : AndroidViewModel(application) {
    private val mRepository: WordRepository = WordRepository(application)
    var mAllWords: LiveData<List<Word>>

    init {
        mAllWords = mRepository.getAllWords()
    }

    fun insert(word: Word) {
        mRepository.insert(word)
    }
}

10. Add XML layout

ここはcodelabの通りに実装します。

layout/activity_main.xml の変更部分で + ボタンが、ぱっとは見つからなかったので、@drawable/ic_android_black_24dp で代用

11. Add a RecyclerView

import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import com.example.hoge.roomwithaview.R
import com.example.hoge.roomwithaview.entity.Word

class WordListAdapter(context: Context): RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {

    class WordViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
        val wordItemView: TextView = itemView.findViewById(R.id.textView)
    }

    private val mInflater: LayoutInflater = LayoutInflater.from(context)
    private var mWords: List<Word>? = null

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
        val itemView = mInflater.inflate(R.layout.recyclerview_item, parent, false)
        return WordViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
        if (mWords != null) {
            val current = mWords!![position]
            holder.wordItemView.text = current.mWord
        } else {
            holder.wordItemView.text = "No Word"
        }
    }

    fun setWords(words: List<Word>) {
        mWords = words
        notifyDataSetChanged()
    }

    override fun getItemCount(): Int {
        return if (mWords != null) {
            mWords!!.size
        } else {
            0
        }
    }
}

13. Add NewWordActivity

レイアウトファイルについては、codelabの通りに実装します。

import android.app.Activity
import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.text.TextUtils
import android.widget.Button
import android.widget.EditText

class NewWordActivity : AppCompatActivity() {

    companion object {
        const val EXTRA_REPLY: String = "com.example.hoge.roomwithaview.REPLY"
    }

    private var mEditWordView: EditText? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_new_word)
        mEditWordView = findViewById(R.id.edit_word)
        val wordText = mEditWordView!!.text

        val button = findViewById<Button>(R.id.button_save)
        button.setOnClickListener {
            val replyIntent = Intent()
            if (TextUtils.isEmpty(wordText)) {
                setResult(Activity.RESULT_CANCELED, replyIntent)
            } else {
                val word = wordText.toString()
                replyIntent.putExtra(EXTRA_REPLY, word)
                setResult(Activity.RESULT_OK, replyIntent)
            }
            finish()
        }
    }
}

14. Connect with the data

import android.app.Activity
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProviders
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import com.example.hoge.roomwithaview.adapter.WordListAdapter
import com.example.hoge.roomwithaview.entity.Word
import com.example.hoge.roomwithaview.model.WordViewModel
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    companion object {
        private const val NEW_WORD_ACTIVITY_REQUEST_CODE = 1
    }

    private var mWordViewModel: WordViewModel? = null

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

        val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
        val adapter = WordListAdapter(this)
        recyclerView.adapter = adapter
        recyclerView.layoutManager = LinearLayoutManager(this)

        mWordViewModel = ViewModelProviders.of(this).get(WordViewModel::class.java)
        mWordViewModel!!.mAllWords.observe(this, Observer {
            adapter.setWords(it!!)
        })

        fab.setOnClickListener {
            val intent = Intent(this, NewWordActivity::class.java)
            startActivityForResult(intent, NEW_WORD_ACTIVITY_REQUEST_CODE)
        }
    }

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

        if (requestCode == NEW_WORD_ACTIVITY_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            val word = Word(data!!.getStringExtra(NewWordActivity.EXTRA_REPLY))
            mWordViewModel!!.insert(word)

            mWordViewModel!!.mAllWords.value!!.forEach {
                Log.i("hoge", it.mWord)
            }
        } else {
            Toast.makeText(
                    applicationContext,
                    R.string.empty_not_saved,
                    Toast.LENGTH_LONG).show()
        }
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate the menu; this adds items to the action bar if it is present.
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        return when (item.itemId) {
            R.id.action_settings -> true
            else -> super.onOptionsItemSelected(item)
        }
    }
}

以上で一通り手順に沿っての実装が完了しました。 早速動作確認してみます。

トップページが表示され、PopulateDbAsyncクラスのdoInBackgroundメソッドによって、HelloWorld という文字が表示されています。

f:id:yuki10k:20181014193133p:plain

Droid君のアイコンをタップすると次の画面に遷移し、文字を入力できます。

f:id:yuki10k:20181014193222p:plain

testと入力し、SAVEボタンをタップするとTOPページに戻り、testの項目が追加されています。

f:id:yuki10k:20181014193319p:plain

一通りの動作確認ができました。 引き続きAndroid開発に携われるように色々学んでいこうと思います。

Android 9 Pieを試してみたくてEssential Phoneを購入

タイトルの通りなのですが、私の保有しているAndroid端末はNexus6が最後に購入したもので、Androidバージョンは7.1.1で止まっていました。 主な原因はPixelシリーズが日本で販売されないためです。 ただ、Pixelシリーズは結構高価なスマートフォンなので、販売されていても購入していたかは微妙・・・

そんな中、Android 9 Pieが突如リリースされ、なんとPixelだけではなくEssential Phoneでも利用可能という事で勢いに任せて購入しました。
技適もあるし。 (どうせPixel 3は日本では販売されないだろうと踏んで・・

www.android.com

今回私が購入したのは、Halo Grayです。
公式サイト経由で購入するより、Amazon.comで購入する方が安く・早く届きそうだったので、今回はAmazon.comで購入しました。

www.amazon.com

$373.44に諸々がプラスされ、$424.97でした。

f:id:yuki10k:20180820224114p:plain

8/20(月) 22時の段階では$223.99まで値下がりしていました。
1週間の差で$150も安くなるとは・・・くやしい!!

f:id:yuki10k:20180820223108p:plain

とはいえ悔やんでも仕方がないので、このスペックでAndroid Pieが使えて$373というだけでも充分安いと自分に言い聞かせ、早速開封していきます。

到着

注文が8/13(月)のお昼頃で到着予定は8/16(木)でしたが、週末しか受け取れなかったので実際に受け取ったのは8/18(土)になります。
海外からの注文だと思えば充分に早いですね。

箱の中には本当に簡単な衝撃吸収材と

f:id:yuki10k:20180820220758j:plain

本体の箱が入っているだけでした。

f:id:yuki10k:20180820220801j:plain

箱自体はとてもシンプルです。

f:id:yuki10k:20180820220804j:plain

裏面にピリピリと剥がしていく所がありました。

f:id:yuki10k:20180820220807j:plain

箱自体は横にスライド式です。

  • 本体
  • 充電器
  • USB Type-Cケーブル
  • イヤホンジャック(USB Type-C)変換ケーブル
  • SIM用のピン等

が入っていました。

f:id:yuki10k:20180820220814j:plain

iPhone X(左)との比較
どちらも黒い上にサイズもほとんど同じで、正面からではぱっと見では違いがわかりません。

f:id:yuki10k:20180820220817j:plain

裏返すと、カメラの方向・指紋リーダー・ブランドロゴなどで違いがはっきりとわかります。

f:id:yuki10k:20180820220820j:plain

設定画面はこんな感じです。
初期のAndroidバージョンは 7.1.1でした。

f:id:yuki10k:20180820220823p:plain

プリインストールアプリも殆ど入っておらず、清々しいです。

f:id:yuki10k:20180820220828p:plain

また、Android 9 Pieはソフトウェアアップデート画面に進むとすぐに降ってきました。

f:id:yuki10k:20180820220826p:plain

1GBほど容量がありましたが、無事にインストール完了。

f:id:yuki10k:20180820220831p:plain

それ以降、少し触った限りではキビキビ動作し、全く不満などはありません。 これから少しづつ触っていき、色々と試してみたいと思います。

Oculus GoをGET!

先日発売となった Oculus GoをようやくGETしました。

www.oculus.com

発売が発表された直後は少し気になりつつも一旦保留にしていました。
というのも以前購入した、スマホをセットするタイプのVRゴーグルを使って、まだこれからだな~と思っていたためです。
しかし、各種レビューを読んだりTwitterを見ているうちに、スタンドアローン型だし試してみようという気になったので、購入に踏み切りました。

購入から到着まで

Oculus Goのサイト上で購入処理を行ったのが 5/7(月)の夜で、 FedExから到着予定日は5/11(金) の夕方になっていました。
結構早いなと思っていましたが、不在票が5/10(木)に入っていました・・
現在はわからないですが、注文から到着まで4日ほどで到着するのは結構早いな~というイメージです。

また、日本語で宛先を入力すると問題がある場合があるとの事だったので、今回は英語で宛先を入力しています。

Expansysでの購入でも何度かお世話になったことがある、こちらのサイトを使って簡単に英語住所に変換できます。

JuDress | 住所→Address変換

開封

ダンボール内もダンボールを折りたたんだもので埋められており、輸送中に動かないようにこていされています。

f:id:yuki10k:20180512214938j:plain

箱を取り出したところ。
今回購入したのは32GBです。

f:id:yuki10k:20180512214940j:plain

蓋を開けると、本体、リモコン、黒い箱が入っています。

f:id:yuki10k:20180512214948j:plain

黒い箱の中にはメガネ用のパーツが入っていました。

f:id:yuki10k:20180512214951j:plain

メガネ用のパーツはこの動画を参考にセットすることができます。

www.youtube.com

さらに黒い箱の奥には、電池、USBケーブル、リモコン用の紐、説明書などが入っています。

f:id:yuki10k:20180512214954j:plain

Oculus Go 本体の液晶側にはOculusアプリをダウンロードしてセットアップを始めようといった文字がプリントされた紙が入っています。

f:id:yuki10k:20180512214957j:plain

Oculusアプリの指示に従うことで簡単にOculusのセットアップができました。

感想

思っていたよりは画質がまだ荒いなと感じる部分もありましたが、なかなかの没入感があります。
また、操作には慣れが必用そう。
しかし、これ単体で動作するというのはなかなか快適で、ベッドで仰向けになりながらNetflixを見たりすることもできました。
これから色々なアプリを試していきたいと思います。

【Android学習】FragmentをKotlinでやってみる

今更感がありますが先日、Udemyを眺めていたところ、以下のコースが目に留まったので、Android開発の学習を進めています。 ※セクション3:Java Deep Diveなど、自分には必用なさそうな部分は省いています。

www.udemy.com

レッスン自体はAndroid N向けの内容のため、最新のAndroid Studioなどではlayoutを作るときに若干勝手が違って苦労する部分もありますが、コース自体はよくできているなという印象です。

しかし、Fragmentを使いたくなったのですがコース内を選択してもヒットしなかったので、こちらの記事を参考に試してみました。

qiita.com

また、Javaでそのまま写経するのもな・・という感じだったので、Kotlinで書いてみました。

レイアウトファイルの作成

まずはFragmentを作成します。
HogeFragmentとかだと味気ないのでにQiitaの例にmakerbrandという項目を追加して、CarDetailFragmentという名前にしてみました。

Fragment作成はapp -> New -> Fragment -> Fragment(Blank)で空のFragmentを作成しました。

レイアウト fragment_car_detail.xml はこんな感じになります。 f:id:yuki10k:20180429213647p:plain f:id:yuki10k:20180429213654p:plain

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

    <!-- TODO: Update blank fragment layout -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView
            android:id="@+id/helloText"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:text="Hello Fragment"
            android:textAlignment="center"
            android:textSize="24sp" />
        <Button
            android:id="@+id/numberButton"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="1" />
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
            <TextView
                android:id="@+id/makerText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="maker" />
            <TextView
                android:id="@+id/brandText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="brand" />
        </LinearLayout>
    </LinearLayout>
</FrameLayout>

Fragmentのコード作成

CarDetailFragment.ktになります。
デフォルトでいろいろメソッドが宣言されているのですが、一旦消して書き直しています。

Kotlinならではのイディオムがあったりしますが、難しいことはないと思います。
断片的に書いていって、最後に全体を書きます。

まずは変数宣言です。
lateinitで後で初期化するよと明示しています。

    private lateinit var mTextView: TextView
    private lateinit var maker: String
    private lateinit var brand: String

続いてcompanion object。
定数やJavaでいうところのstaticメソッドはここに定義しました。
createInstanceメソッドでは、インスタンス生成時にパラメーターを受け取って、Bundleに渡してFragmentのargumentsにセットしています。

    companion object {
        private const val KEY_MAKER = "maker_name"
        private const val KEY_BRAND = "brand_name"

        fun createInstance(maker:String, brand:String): CarDetailFragment {
            val carDetailFragment = CarDetailFragment()
            val args = Bundle()
            args.putString(KEY_MAKER, maker)
            args.putString(KEY_BRAND, brand)
            carDetailFragment.arguments = args
            return carDetailFragment
        }
    }

あとはFragmentのライフサイクルメソッドを実装していきます。
ライフサイクルは公式の図がわかりやすいと思います。

developer.android.com

今回は、

  • onCreate ・・・ パラメータを受け取ってメンバ変数にセット
  • onCreateView ・・・ Viewの生成
  • onViewCreated ・・・ View生成後の操作

を実装しています。
全体は↓こんな感じになりました。

// packageは省略

import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView

class CarDetailFragment : Fragment() {

    private lateinit var mTextView: TextView
    private lateinit var maker: String
    private lateinit var brand: String

    companion object {
        private const val KEY_MAKER = "maker_name"
        private const val KEY_BRAND = "brand_name"

        fun createInstance(maker:String, brand:String): CarDetailFragment {
            val carDetailFragment = CarDetailFragment()
            val args = Bundle()
            args.putString(KEY_MAKER, maker)
            args.putString(KEY_BRAND, brand)
            carDetailFragment.arguments = args
            return carDetailFragment
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val args = arguments
        if (args == null) {
            maker = ""
            brand = ""
        } else {
            maker = args.getString(KEY_MAKER)
            brand = args.getString(KEY_BRAND)
        }
    }

    override fun onCreateView(inflater: LayoutInflater,
                              container: ViewGroup?, savedInstanceState: Bundle?): View? {
        super.onCreateView(inflater, container, savedInstanceState)
        return inflater.inflate(R.layout.fragment_car_detail, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        mTextView = view.findViewById(R.id.helloText)
        view.findViewById<Button>(R.id.numberButton).setOnClickListener {
            mTextView.text = "${mTextView.text}!"
        }

        val makerText = view.findViewById<TextView>(R.id.makerText)
        makerText.text = maker

        val brandView = view.findViewById<TextView>(R.id.brandText)
        brandView.text = brand
    }
}

Fragmentを呼び出すActivityの作成

こちらもFragmentと同じように app -> New -> Activity -> Blank Activityで作成します。

LinearLayoutだけ定義し、中身は空っぽにしておきます。

f:id:yuki10k:20180429221650p:plain

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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=".CarDetailActivity">

    <LinearLayout
        android:id="@+id/carDetailContainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"></LinearLayout>
</android.support.constraint.ConstraintLayout>

CarDetailActivity.ktは以下のような感じになりました。
パラメーターとして

という文字をFragmentに対して渡しています。

// packageは省略

import android.support.v7.app.AppCompatActivity
import android.os.Bundle

class CarDetailActivity : AppCompatActivity() {

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

        // コードからフラグメントを追加
        if (savedInstanceState == null) {
            val transaction = supportFragmentManager.beginTransaction()
            transaction.add(R.id.carDetailContainer, CarDetailFragment.createInstance("TOYOTA", "プリウス"))
            transaction.commit()
        }
    }
}

実際に実行して、CarDetailActivityを開くと、以下のようにFragmentの内容が表示され、パラメータの受け渡しが行われていることが確認できました。

f:id:yuki10k:20180429222155p:plain

最初はなかなかとっつきにくいなーと思っていましたが、実際に触ってコードを書いていくと、「あーなるほど」という感じで少しづつ理解ができてきました。

引き続き時間を見つけて色々実装を試してみたいと思います。

Air Podsを買ったら最高だった

Air Podsを買いました。

www.apple.com

前々から欲しいなとは思っていましたが値段が値段なのでなかなか踏ん切りがつかず、Ear Podsをずっと使っていました。
何度かBluetoothイヤホンを使ったことがある身としては、iPhoneから伸びるEar Podsのケーブルは非常に邪魔で、しょっちゅうイライラしていました。
また、先端がLightningケーブルになっているため、Macには刺せないのも不便だなと感じていました。

そんな中、AppleStoreギフト券を貰ったのでせっかくのチャンスなのでAir Podsの購入に踏み切ることに。 「2018年には進化したAir Podsが出る」といった噂や、「ケースがAir Powerに対応して発売される」といった噂があり正直迷いましたが、Air Power充電は別にいいかなと思い、購入しました。

早速Apple Storeを見ていると、ネット注文では7営業日・・・
しかし、店舗だと在庫があるようで、直接店舗に行って購入することに。

無事にゲット。
アクセサリでもAppleの商品を開封するときのワクワク感はたまりません。

蓋を開けると恒例の「Designed by Apple in California」

さらに奥に進むと、Air Podsケースを発見

その下にはLightningケーブルが入っていました。

ついにケースとご対面。

早速ケースを開けると・・・
iPhoneが反応し、Air Podsとの接続確認ダイアログが出現。
接続ボタンを押すとすぐにiPhoneAir Podsの接続が完了しました。
このユーザー体験はAppleならではと関心します。

最後に本体をケースから取り出してみました。
若干マグネットになっており、蓋を空けてもケースから落としにくい親切設計。

同じApple IDの端末にはシームレスに切り替わるはずですが、複数のデバイスが近くにある場合には、iPhoneMacからAirPodsに接続してあげる必要がありました。

iPhoneの場合、「コントロールパネル?」からミュージックパネルの右上のアイコンを選択すると

音を出すデバイスが選べるので、Air Podsを選択するとAid Podsと接続ができます。

また、Bluetoothの接続画面から、Air Podsのダブルタップ時の動作も設定できます。
Siriを割り当てることもできますが、「再生/一時停止」を割り当てておくと、iPhoneに触ること無くAir Podsを耳にセットしてAir Podsをダブルタップすると音楽が再生されるのでかなり便利です。

「耳からうどん」などと言われることもありますが、最近では街中でAir Podsを耳に着けているユーザーもちらほら見かけるようになってきたので、あまり目立つこともなくて良いかも。

まだまだ使い倒せていないですが、これからどんどん使い倒していきたいと思います。