20191224のAndroidに関する記事は8件です。

ColumnとRowで理解するJetpack Composeのレイアウト基礎

はじめに

Cloud NextでJetpack Composeは発表され、先日Android Dev Summitでは、大幅なアップデート行われました。

Jetpack Composeのレイアウトを制御するコンポーネントは以下のような階層構造になっています。
下位の階層ほど汎用的なコンポーネントになっています。
上位のコンポーネントは、下位のコンポーネントをラッパーしたものになっており、指定できる機能が限定されています。

jc1.png

これらの階層構造から実際にそれぞれの役割がどのように異なるのかを理解していきたいと思いますが、今回はまずColumnとRowの実際の挙動を確認することで、ColumnとRowの役割を明確にすることでFlexとの違いを理解する第一歩としたいと思います。

Column基本

縦に内部の要素が並びます。

    Column {
        (1..50).toList().map { it.toString() }.forEach { name ->
            Button(name)
        }
    }

Screenshot_1577164971.png

ColumnにLayoutModifierを指定

引数modifilerにはLayoutModifier型を指定して、カラムのサイズなどを指定できます。
指定しない場合は、Modifier.None が指定されます。

modifierにWidthを指定

カラムの幅が40dpになるのでボタンもそれに伴い縮びます。

    Column(modifier = Width(40.dp)){
        (1..50).toList().map { it.toString() }.forEach { name ->
            Button(name)
        }
    }

Screenshot_1577166390.png

modifierにHeightを指定

カラムの高さが400dpになるので、一部のボタンが表示されません。

    Column(modifier = Height(400.dp)){
        (1..50).toList().map { it.toString() }.forEach { name ->
            Button(name)
        }
    }

Screenshot_1577166678.png

WidthとHeightを指定する場合は、Sizeを利用します。

    Column(modifier = Size(width = 400.dp, height = 100.dp)){
        (1..50).toList().map { it.toString() }.forEach { name ->
            Button(name)
        }
    }

modifierにExpandedを指定

利用できるスペースをすべて使うように拡大されます。今回の例ではデフォルトのNoneとの違いわかりませんでした。
他にはExpandedHeight、ExpandedWidthも存在します。

    Column(modifier = Expanded){
        (1..50).toList().map { it.toString() }.forEach { name ->
            Button(name)
        }
    }

Screenshot_1577166941.png

wrapを用いて複数のmodifierの指定

最初に書いた方が優先されるので、以下の例ではSizeで指定した高さ10px、はHeightで指定した高さ100.dpに上書きされます。

    Column(modifier = Height(100.dp) wraps Size(width = 100.dp , height = 10.dp)){
        (1..50).toList().map { it.toString() }.forEach { name ->
            Button(name)
        }
    }

Screenshot_1577169640.png

他にもModifierが用意されていそうですが、すべてを一覧で確認するドキュメントが見つからなかったためわかりませんでした。

ColumnにArrangementを指定

arrangementは配置を意味します。
デフォルトでは Arrangement.Begin が指定されています。

以降の例では、Column自体が内部のコンテンツのサイズに縮まってしまうと表示結果に違いがでないため、modifilerにExpandedを指定して説明します。

arrangementにArrangement.Endを指定

終端に並びます。

    Column(
        modifier = Expanded,
        arrangement = Arrangement.End
    ){
        (1..3).toList().map { it.toString() }.forEach { name ->
            Button(name)
        }
    }

Screenshot_1577167692.png

arrangementにArrangement.Centerを指定

中央に並びます。

    Column(
        modifier = Expanded,
        arrangement = Arrangement.Center
    ){
        (1..3).toList().map { it.toString() }.forEach { name ->
            Button(name)
        }
    }

Screenshot_1577167842.png

arrangementにArrangement.SpaceAroundを指定

子要素間に等間隔に並べます。開始地点と子要素、子要素と終了地点の間は子要素間のスペースの半分になります。

    Column(
        modifier = Expanded,
        arrangement = Arrangement.SpaceAround
    ){
        (1..3).toList().map { it.toString() }.forEach { name ->
            Button(name)
        }
    }

Screenshot_1577167896.png

arrangementにArrangement.SpaceBetweenを指定

子要素間が等間隔に並びます。開始地点、終了地点のスペースはありません。

    Column(
        modifier = Expanded,
        arrangement = Arrangement.SpaceBetween
    ){
        (1..3).toList().map { it.toString() }.forEach { name ->
            Button(name)
        }
    }

Screenshot_1577167953.png

arrangementにArrangement.SpaceEvenlyを指定

開始地点、子要素間、終了地点が等間隔に並びます。

    Column(
        modifier = Expanded,
        arrangement = Arrangement.SpaceEvenly
    ){
        (1..3).toList().map { it.toString() }.forEach { name ->
            Button(name)
        }
    }

Screenshot_1577167992.png

Rowとの違い

RowについてもColumnが縦に子要素を並べるのに対して、横に並べるだけで挙動は同じです。

Screenshot_1577165219.png

Flexとの違い

FlexにはColumnに存在しないMainAxisAlignmentとCrossAxisAlignmentが存在します。

MainAxisAlignmentは内部的にはArrangementを持っており、また FlexはFlexLayoutの arrangement = mainAxisAlignment.arrangement を渡していることからも同じ役割であることがわかります。

CrossAxisAlignmentは、新規に追加されている値で、おそらくColumnの場合は横方向、Rowの場合は縦方向にレイアウトの制御を行うものだと考えられます。

まとめ

Columnを例にLayoutModifierとArrangementについて説明しました。
Column, RowとFlexの違いはCrossAxisAlignmentにあることがわかりました。

次回はFlex、FlexColumn、FlexRowを調査します。

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

RecyclerView上のボタンを押した時の処理をリストごとに変化させる

はじめに

Androidアプリ開発でリスト表示を作るためにRecyclerViewを利用しますよね。
RecyclerView上にボタンを設置して、リストから他の画面に遷移する時、何番目のリストのボタンが押されたかを知りたい時ありますよね。
RecyclerView.Adapter上で「何番目のボタンが押されたか」は割とすぐ取得できるのですが、それをMainActivityで使う時に少しハマったのでメモ。

RecyclerViewについて

概要についてはこちらの記事が分かりやすかったのでこちらを参照ください。
RecyclerViewの基本

やりたいこと

  • RecyclerView上にボタンを配置し、ボタンを押した時に画面遷移させたい。
  • ボタンを押したリストの行数によって遷移先での挙動を変化させたい。

画面遷移する時のボタンを押した時の処理のイメージはこんな感じ。
遷移先の画面に対して、何番目のリストのボタンが押されたかを教えてあげたい

ちなみにRecyclerView.AdapterはActivtyクラスを継承していないので画面遷移ができません。
そのため、MainActivity上で何番目のリストが押されたかを知り、画面遷移することが必要になります。

MainActivity
@Override                                                                             
public void onClick(View view) {                                                      
    int line = getLine(); //どうにかして押したリストの行数を取得したい                             
    Intent hoge = new Intent(MainActivity.this, HogeActivity.class);
    hoge.putExtra("line", line);                   
    startActivity(hoge);                                                              
}    

まずその前に

RecyclerViewに含まれるボタンを押した時に、MainActivity上での処理を行いたいので
RecyclerView.AdapterのリスナーにMainActivityのOnClickListenerを登録しておきます。
これでリスト上でボタンを押した時に、MainActivty上のOnClick()が呼び出されるようリスナーの設定ができます。

RecyclerAdapter.java
public void setOnItemClickListener(View.OnClickListener listener){
        m_listener = listener;
}

public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
    // - get element from your dataset at this position
    // - replace the contents of the view with that element
    final int pos = position;
    // リスト上のボタンが押された時に上で登録したクリックリスナーを呼び出してあげる
    holder.m_btn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            m_listener.onClick(view);
        }
    });
}
MainActivity.java
RecyclerView.Adapter rAdapter = new RecycleAdapter(data);
rAdapter.setOnItemClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        int line = getLine(); //どうにかして押したリストの行数を取得したい                             
        Intent hoge = new Intent(MainActivity.this, HogeActivity.class);
        hoge.putExtra("line", line);                   
        startActivity(hoge);  
    }
});    

本題

今の状態のままだと、どのリストのボタンを押しても同じ処理が行われてしまいます。

RecyclerView.AdapterにonBindViewHolderというメソッドがあります。
これはリストの項目が作られるたびに呼び出されるメソッドです。
そして引数であるpositionがまさに何番目のリストが作成されたか、という値を表しています。

このpositionの値を、リスト上のボタンのクリックリスナー内でRecyclerViewのメンバ変数に登録するよう実装します。
こうすることでボタンが押された時にその行数の値がRecycleViewAdapterのメンバ変数(m_line)に登録されます。

そしてそのm_lineを外部から参照できるようなメソッドを作成し、それをMainActivityのOnClick()内で呼び出すよう実装します。
あとは遷移先のHogeActivityで値を取得して、それによって処理を変えてあげればOKですね。

RecyclerAdapter.java
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
    // - get element from your dataset at this position
    // - replace the contents of the view with that element
    final int pos = position;
    holder.m_btn.setOnClickListener(new View.OnClickListener() { 
        @Override
        public void onClick(View view) {
            m_line = pos; //行数を登録
            m_listener.onClick(view); //登録した直後にMainのOnClickを呼び出す
        }
    });
}

public int getLine(){
    return m_line; //行数を取得
}
MainActivity.java
final RecyclerView.Adapter rAdapter = new RecycleAdapter(list);
recyclerView.setAdapter(rAdapter);
((RecycleAdapter) rAdapter).setOnItemClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        int line = ((RecycleAdapter) rAdapter).getLine(); //ここでm_lineの値を取得
        Intent edit = new Intent(MainActivity.this, HogeActivity.class);
        edit.putExtra("line", line);
        startActivity(edit);
    }
});

最後に

なんだかものすごく回りくどい実装になってしまいましたが
押した行数をMainActivityに伝える方法が自力ではこれしか見つからなかったのでこんな実装になってしまいました。
「いやいや、そんなことしなくてもhogehogeすれば一発やん...」というのがあればぜひご教授ください。

最後の最後に

自分が作ったアプリからコード抜粋して少し修正しているので、文章とソースコードの変数名が一致していなかったりする場合もあります。
そこは脳内変換お願いします。

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

Camera Xを試す

Camera Xとは

CameraX は、カメラアプリの開発を簡単に行うための Jetpack サポート ライブラリです。ほとんどの Android デバイスで機能する、使いやすく一貫性のある API サーフェスを提供するほか、Android 5.0(API レベル 21)への下位互換性も備えています。

これまでのcamera2よりもシンプルに実装でき、Android 5.0でも使用することができるので嬉しいですね。
また、拡張機能を利用することでポートレート、HDR、夜景などの効果も利用することができるようです :camera_with_flash:
https://developer.android.com/training/camerax

公式でCodelabが用意されているので、今回はそれを進めていきたいと思います。
https://codelabs.developers.google.com/codelabs/camerax-getting-started

完成図↓
mojikyo45_640-2.gif

環境

  • Kotlin
  • minSdkVersion 21
  • Android Studio 3.3(以上)

1. Gradleへ依存関係を追加

build.gradle
def camerax_version = '1.0.0-alpha06'
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"

現時点ではimplementation 'androidx.appcompat:appcompat:1.1.0'が必要でした。

また、Camera XのいくつかのメソッドはJava 8が必要なので、コンパイルオプションを設定する必要があります。
androidブロックの最下部に以下を追加します。

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

2. レイアウト作成

プレビュー画面表示用のTextureViewを設定します。

activity_main.xml
<?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=".MainActivity">

    <TextureView
        android:id="@+id/view_finder"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

3. カメラのパーミッションをリクエスト

AndroidManifest.xmlのapplicationタグの前にCameraパーミッションを追加します。

AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" />

次に、カメラへのアクセス許可の要求を実装していきます。

MainActivity.kt
class MainActivity : AppCompatActivity() {

    companion object {
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
    }

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

        viewFinder = findViewById(R.id.view_finder)

        if (allPermissionsGranted()) {
            viewFinder.post { startCamera() }
        } else {
            ActivityCompat.requestPermissions(
                this,
                REQUIRED_PERMISSIONS,
                REQUEST_CODE_PERMISSIONS
            )
        }
    }

    // パーミッションダイアログの結果を取得
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                viewFinder.post { startCamera() }
            } else {
                Toast.makeText(this, "Permissions not granted by the user.", Toast.LENGTH_SHORT)
                    .show()
                finish()
            }
        }
    }

    // マニフェストで指定されたすべての権限が付与されているか
    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            baseContext, it
        ) == PackageManager.PERMISSION_GRANTED
    }
}

4. ViewFinderの実装

Previewクラスを使用します。

MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ...
        ...
        // TextureViewが変更される度にレイアウトを再計算
        viewFinder.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
            updateTransform()
        }
    }

    private fun startCamera() {
        // PreviewConfigオブジェクトを生成
        val previewConfig = PreviewConfig.Builder().apply {
            setTargetResolution(Size(640, 480))
        }.build()

        // PreviewConfigからPreviewインスタンスを生成
        val preview = Preview(previewConfig)
        preview.setOnPreviewOutputUpdateListener {
            val parent = viewFinder.parent as ViewGroup
            parent.removeView(viewFinder)
            parent.addView(viewFinder, 0)

            viewFinder.surfaceTexture = it.surfaceTexture
            updateTransform()
        }
        // Camera Xのライフサイクルにバインド
        CameraX.bindToLifecycle(this, preview)
    }

    private fun updateTransform() {
        val matrix = Matrix()
        // ViewFinderの中心を計算
        val centerX = viewFinder.width / 2f
        val centerY = viewFinder.height / 2f
        // ディスプレイの回転を考慮してプレビューを出力
        val rotationDegrees = when (viewFinder.display.rotation) {
            Surface.ROTATION_0 -> 0
            Surface.ROTATION_90 -> 90
            Surface.ROTATION_180 -> 180
            Surface.ROTATION_270 -> 270
            else -> return
        }
        matrix.postRotate(-rotationDegrees.toFloat(), centerX, centerY)
        // TextureViewに変換をを適用
        viewFinder.setTransform(matrix)
    }
}

ここまでで、カメラが表示されるようになったとおもいます :iphone:

5. 画像キャプチャ

撮影用のボタンを設置します。

activity_main.xml
<ImageButton
    android:id="@+id/capture_button"
    android:layout_width="72dp"
    android:layout_height="72dp"
    android:layout_margin="24dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:srcCompat="@android:drawable/ic_menu_camera" />

カメラボタンを押した時の保存処理を追加します。

MainActivity.kt
    private fun startCamera() {
        ・・・
        ・・・
        // ImageCaptureConfigオブジェクトを生成
        val imageCaptureConfig = ImageCaptureConfig.Builder().apply {
            setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
        }.build()

        // ImageCaptureConfigからImageCaptureインスタンスを生成
        val imageCapture = ImageCapture(imageCaptureConfig)
        findViewById<ImageButton>(R.id.capture_button).setOnClickListener {
            val file = File(externalMediaDirs.first(), "${System.currentTimeMillis()}.jpg")

            // 画像をキャプチャしてファイルに保存
            imageCapture.takePicture(file, executor,
                object : ImageCapture.OnImageSavedListener {
                    override fun onError(
                        imageCaptureError: ImageCapture.ImageCaptureError,
                        message: String,
                        cause: Throwable?
                    ) {
                        val msg = "Photo capture failed: $message"
                        Log.e("Camera X App", msg, cause)
                        viewFinder.post {
                            Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
                                .show()
                        }
                    }

                    override fun onImageSaved(file: File) {
                        val msg = "Photo capture succeeded: ${file.absolutePath}"
                        Log.d("Camera X App", msg)
                        viewFinder.post {
                            Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
                                .show()
                        }
                    }
                })
        }
        // 引数に"imageCapture"を追加
        CameraX.bindToLifecycle(this, preview, imageCapture)
    }

ボタンを押すと撮影した画像が保存されているはずです :file_folder:

6. 画像解析

最後にImageAnalysisクラスでちょっとした解析を行ってみましょう。
ここでは画像の平均輝度を求めてみます。

MainActivity.kt
private class LuminosityAnalyzer : ImageAnalysis.Analyzer {
    private var lastAnalyzedTimestamp = 0L

    private fun ByteBuffer.toByteArray(): ByteArray {
        rewind()
        val data = ByteArray(remaining())
        get(data)
        return data
    }

    // ImageAnalysis.Analyzerインターフェースにある"analyze"メソッドをオーバーライド
    override fun analyze(image: ImageProxy, rotationDegrees: Int) {
        val currentTimestamp = System.currentTimeMillis()
        // 輝度計算を毎秒以上の間隔で行う
        if (currentTimestamp - lastAnalyzedTimestamp >= TimeUnit.SECONDS.toMillis(1)) {
            val buffer = image.planes[0].buffer
            val data = buffer.toByteArray()
            val pixels = data.map { it.toInt() and 0xFF }
            val luma = pixels.average()
            Log.d("Camera X App", "Average luminosity: $luma")
            lastAnalyzedTimestamp = currentTimestamp
        }
    }
}

startCamera()メソッド内でインスタンスの生成を行い、それをバインドします。

MainActivity.kt
private fun startCamera() {
    ・・・
    ・・・
    // ImageAnalysisConfigオブジェクトを生成
    val analyzerConfig = ImageAnalysisConfig.Builder().apply {
        setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
    }.build()

    // ImageAnalysisConfigからImageAnalysisインスタンスを生成
    val analyzerUsecase = ImageAnalysis(analyzerConfig).apply {
        setAnalyzer(executor, LuminosityAnalyzer())
    }
    // 引数に"analyzerUsecase"を追加
    CameraX.bindToLifecycle(this, preview, imageCapture, analyzerUsecase)
}

これで完了です。
以下のようなログが出力されるようになりましたね :tada:

D/Camera X App: Average luminosity: 86.584970703125
D/Camera X App: Average luminosity: 86.58956380208333
D/Camera X App: Average luminosity: 91.70888997395834
D/Camera X App: Average luminosity: 91.86529947916667
D/Camera X App: Average luminosity: 92.1453125

最後に

カメラ機能の実装がとても楽になり、開発工数の削減にも繋がるかもしれませんね!
より詳細な解析や、ポートレート効果なども実装してみたいと思いました :relieved:

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

Google Play Playtime 2019 潜入レポと今後やりたくなったこと

はじめに

初めまして!NewsPicksのPMをしています、片山と申します。
私は10月にNewsPicksに入社しその前は10代女性向けメディアのWEBディレクターをやっておりました。その中でも主に担当していたことはADNWによるマネタイズでしたが、媒体のユーザーインタビュー、数千人規模の定量調査、媒体売りの営業、オウンドメディアの運営を少し、オウンドメディアから派生した商品開発コンサルタント、新作映画の取材など、、かなり浅いですがwいろんな経験をさせていただいておりました。

10月に入社して最初にUIリニューアルプロジェクトに携わり、現在はそれに伴ったアプリストアの掲載情報のアップデートに取り組んでいます。

さて、今回書くテーマは12/6にGoogleの主催で開催された「Google Playtime 2019」に行ってきた体験記です。
今まで私は、Googleの主催するイベントには数多く出席させていただきましたが、毎度「参加してよかった」と思える事ばかりだったので今回もきっと期待以上の情報が得られるのではないか?と思った事もあり参加を強く希望しました。

参加の前にGoogle Playtimeについて下調べしていたのですが、あまり情報が出回っていなかったので「参加後はこの体験を共有したい」と思っていました。是非とも次回開催の際に役立てていただけたら嬉しいです!

目次

Playtime 2019とは

Playtime は、年に 一度、Google Play のトップパートナー企業の経営層、アプリ事業責任者を対象とした、Google Play が主催する、招待者限定のフラッグシップイベントです。今回のPlaytimeは今年で6回目ということで、大盛況でした。
内容はGoogle Playの活用術や最新情報の共有と、1対1での相談ブース、課題を共有するグループディスカッション、ツールを使用した Q&A セッションなど、国内外のGoogle担当者やエキスパートに直接会話ができる場所となっていて、まさにインタラクティブな場所でした。英語ができるか心配・・みたいな方でも英語のセッションでは1人1台、同時通訳機の貸し出しがあり、言葉の問題は全くなく内容に集中してお話を聞くことができました。また、会場に来ている方々とのネットワーキングができるスペースも常に開かれており参加者のみなさんはうまく活用している様子でした。
アプリとゲームに分かれたセッションの後には、懇親会と GooglePlay2019年ベストアプリ、ゲームの各カテゴリ大賞受賞者発表と授賞式も盛大に行われました。

1日の流れ

今回のPlayTime2019の場所はグランドハイアット。六本木ヒルズに隣接している素敵なホテルです!
image.png
Googleはまるっと渋谷ストリームに移転しましたが、今回はヒルズに隣接したこちらの会場でした。NewsPicksは六本木にオフィスがあるので歩いて行きました。

12:00 - 13:10 受付

受付では登録の名前順でレーンが整備されており名刺をお渡しすると、名前が記名されたカードをいただきました。

思ったよりもかなりスムーズに受付完了

受付の近くには他に、2019年にベストアプリとしてノミネートしていたアプリアイコンが部門ごとに大きくスペースをとって掲載されていました。

iOS の画像 (5).jpg

クロークが会場にあったので荷物はそこで預けます。コートなどは邪魔になりがちなところ、その辺の気遣いが嬉しかったです><

早速インタラクティブセッションの案内を発見。

iOS の画像 (9).jpg

このインタラクティブセッションとは講義型の一方的なセッションとは違い、少人数制でより詳細の質問などが行き交うセッション形態を指しているようで1つのインタラクティブセッションごとに決まった枚数の整理券を配布していました。まるで大学の講義のように時間割を自分でカスタマイズできるといったところでしょうか。

早めに受付を済ませていた先輩社員が整理券を数枚ゲットしてくださっていたのでなんとか聞きたいものが聞くことができました!

コンテンツが始まるまではオープンスペースで立食のウェルカムドリンクと軽食のサービス!このサービスは1日を通して利用ができるみたいで、一緒にいた同僚も大興奮!笑
IMG_3938.PNG

そうこうしているうちに会場がOPENしたので着席します。

13:10 - 13:30 Playtime 2019 開催のご挨拶
13:30 - 14:50 基調講演、Android と Google Play より最新情報、ゲスト セッション

IMG_3432.jpg

まずはじめに、GooglePlay Head of Japan AppsのEvan Kojimaさんよりお話がありました。
こちらでは2019年のアプリのトレンドについての傾向と振り返りとともにNTTドコモの方からの基調講演では「5Gがやってくる日本の未来」をわかりやすくまたかっこいい動画も合わせて発表していただきました。

15:20 - 17:10 セッション

アプリに関するセッションとゲームに関するセッションに会場をわけ、来場者は聞きたい内容を選択し聞くことができます。
また、受付前にゲットしたチケットを持って参加する、インタラクティブセッションもこの時間に開催されていました。

17:20 - 17:50 Google Play Q & A セッション

Sli.doを活用してインタラクティブなQ&Aが行われていました。
内容の共有はできませんが、登壇者の方が丁寧に一つずつ質問に回答してくださっておりまさにインタラクティブで有意義な時間となりました。
IMG_3452.jpg

18:00 - 20:00 懇親会 / ベストオブ 2019 授賞式

セッション会場外でのオープンスペースでは立食の懇親会が行われ、様々な企業のマーケター・エンジニア・PMが情報交換の場として活用していました。
しばらくすると授賞式が始まり、アプリ(ユーザー投票部門・生活お役立ち部門・自己改善部門・隠れた名作部門・エンターテイメント部門)
とゲーム(ユーザー投票部門・エキサイティング部門・インディー部門・キュート&カジュアル部門・クリエイティブ部門)で各種表彰がありました。
IMG_3456.jpg
iOS の画像 (17).jpg

ゲーム部門での最優秀ゲームアプリとしてドラゴンクエストウォークが表彰されドラクエの生みの親、堀井雄二さんが受賞のコメントをお話されていました。
iOS の画像 (10).jpg

セッション内容

「PlayTime(遊び時間)」というように本当に楽しい雰囲気に終始包まれ、まだ触ったことのないおもちゃを与えられたようなワクワクした感覚になりました^^

私が参加したセッションは2つ。

  • ユーザーを細かく分析する上で大切なこと(五十嵐 郁様)
  • サブスクリプション成功への第一歩〜最新情報から読み解くサブスクリプションサービス成長に必要なこと〜(早乙女 太郎様)

また、上記の他に「ストア掲載情報の最適化」という内容のインタラクティブセッションに参加しました。
IMG_3449.jpg

ユーザーを細かく分析する上で大切なこと

iOS の画像 (6).jpg

このセッションでは主に、ゲームの課金マネタイズによる媒体売上と成長が伸び悩んでいる媒体に向けた内容でした。当初は「NewsPicksにはあまり関係ないお話かな?」とも思っていましたが、媒体者として通じるところもたくさんある内容であったので勉強になることばかりでとても有意義なものとなりました。特に私はゲーム媒体を担当したことがいいままでなかったので初めて見る指標もあり、いつも見ている角度とは別の角度でNewsPicksを俯瞰できるいいきっかけとなったセッションでした。

サブスクリプション成功への第一歩

iOS の画像 (7).jpg
このセッションでは、主にサブスクリプションを導入されていない媒体向けに内容の用意をされていたようでしたがサブスクリプションアプリ市場の最新情報と、サブスクリプション市場の活性化をはかる新たなストアのアップデート情報&他社事例をもとにこれからのサービス成長のヒントを紹介してくれました。他社の事例で成功した実績がわかりやすく発表されていたことでより現実的に検討することができ、社内にすぐ共有したくなるような目から鱗のヒントをたくさん紹介してくれました。

今後やりたくなったこと

NewsPicksはソーシャル経済メディアとして、ユーザーの方と共にたくさんの素晴らしいコンテンツを生み出し続けています。
また、サービス価値のもう一方の側面には、アプリがもたらす総合的な体験としての価値があります。アプリの体験を向上させていくため、先端のトレンドをしっかりと把握し、また分析や調査をしっかりと行い、それらに基づくたゆまぬ改善を求められていることを、あらためて実感しました。
今回のPlaytime 2019で得た最新情報と施策・開発のヒントを踏まえ、より探し求めているコンテンツに出会いやすくユーザーの方の行動に寄り添える改善と進化を心がけコンテンツとアプリの両面からNO.1のアプリだといっていただけるようにしていきたいものです。
そしてひいては、たくさんのかたに手にとっていただきその中から生まれた新たなアイデアで世界が変えることができればと思っています。

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

Bitriseからアプリをチームにテスト配布するための設定

この記事はBitriseアドベントカレンダーの23日目の記事です。

どうも!ハムカツおじさんという名前でtwitterやってます(@hmktsu)?
自分でだったり弊社でだったりなどでBitriseを使ってサービスをビルドしています。

ちなみに先日Bitrise User Group Meetup #3にて以下のスライドを発表させていただいたので、興味ある人はご覧になってください!
XcodeやAndroid Studioを弄らないアイコン管理

はじめに

Bitriseはアプリ開発をするにあたって外せないサービスになっていると思っています。
対抗馬としてCircleCIなどの他サービスもありますが、iOSやAndroidのビルドをするためにブラウザ上でぽちぽちするだけでworkflowを組め、さらにはBetaみたいにBitriseを使ってのAdhoc配信や各種Appストアへのアップロードも行ってくれます。

そんなBitriseを使っていてチーム内でアプリのAdhoc配布をしたいということは多々あります。
なので今日はチームに配布するために色々と設定するべき項目、やってもらうべき項目についてを書きたいと思います。

なお諸々の細かい設定に関しては、ほぼほぼiOS向けとなっております。

配布されたい人がアカウントを作成

  • アカウントを作成して管理者にアカウント名を伝える
  • Account Settings -> Test devicesをiOSのSafariで開いてデバイスを登録

Bitriseのアカウントを作成しましょう。
次にBitriseのチームを管理している人に自分のアカウント名をお知らせしてください。

その後iOSをBitriseに登録する必要があります。
SafariでBitriseを開き、Account Settings -> Test devicesを開いてください。
プロファイルをダウンロード -> iOSの設定 -> プロファイル -> インストール -> 登録完了となります。

Safariをプライベートモードで開いているとうまくいかないので注意

配布するための設定

  • Organization -> Groupの作成
  • Groupにアカウントを追加
  • アプリのダッシュボード -> Teamで先ほど作ったGroupをTESTERS / QAに追加
  • Workflowに「iOS Auto Provision」と「Deploy to Bitrise.io - Apps, Logs, Artifacts」を追加
  • ビルド

BitriseのOrganizationにおいて、Groupを作成しましょう。
初期状態でいくつかGroupが作成されていますが、配布したいアプリ用のGroupを作っておくと個人的には管理が楽だなぁと思います。

スクリーンショット 2019-12-24 11.46.28.png
Groupを作ったら先ほど教えてもらったアカウント名を作ったGroupに追加してください。

スクリーンショット 2019-12-24 11.34.07.png
次にアプリのダッシュボード -> Teamに先ほどのGroupをTESTERS / QAに追加しましょう。

スクリーンショット 2019-12-24 11.42.58.png
「iOS Auto Provision」と「Deploy to Bitrise.io - Apps, Logs, Artifacts」をWorkflowに追加し、諸々を設定してください。

以上の設定をしてビルドをすると、自動的にGroupに追加したユーザ宛てにダウンロードURLがメールで飛びます。

ビルドページからインストール

  • Click hereというリンクをクリックしてプロファイルをダウンロード
  • 設定 -> プロファイル -> インストール
  • Safariが開くのでインストールボタンを押してインストール

Download anywayというボタンを押すとipaをインストールするけど、これはシミュレータ用とか他の用途なので、プロファイル経由でないといけないので注意

まとめ

このような感じでGroupを作成して追加/削除をするだけで配布されるかどうかということが決まるのでとても簡単です。
特にBetaに渡すとかそういうこともせずにBitriseだけでDEV環境の確認ができ、App StoreからTest FlightでPROD環境の確認をするという流れできます。
できる限り自分が管理するサービスを少なくするための選択肢としてBitriseはいいんじゃないかと思います。

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

【Kotlin】初心者によるAndroid開発 〜困った編〜

はじめに

初心者(プログラミング初学者)あるあるかもしれませんが、
私の失敗談だったり、困った出来事を簡単にまとめて行きます。
「初心者のとき、そんなこともあったなー」と、共感してくれる方がいると幸いです。
それだけで私が救われます

恐怖のJava

一番悩まされ、幾度となく机ドンする羽目になった私殺しのJava。
Kotlinがプログラミング初学の私にとっては強敵でした...

まず、私はJavaとKotlinで書かれているコードの違いがわかりませんでした。
その結果『Kotlin Android Listview』とかで調べた良い感じのコードを当てはめてもうまく動かない事がありました。

悩みました。正直。Kotlinで検索してもJavaのコードが紛れ込んでいるからです。
ただ、よくよく見ていくと、決定的な違いが有りました。それは...

Javaは、命令文の終わりに『;』が付いている

これですよ!!気づいてからはJavaのコードをすぐに見破れるので本当によかったです。
ちなみに初めて買った参考書はKotlinのつもりが、Javaだったよ...そりゃうまく動かないよね!!

AndroidX vs SupportLibrary(V7)

出版が新しい参考書と公式ドキュメントを参考にして勉強を進めていくと、基本的にはandroidXを使う流れになると思います。しかし、参考書や参考サイトが古いとV7で書かれていることがありました。

import androidxは知っているがimport android.support.v7は完全に初見。
それでいて後述する文章が違う場合もあったりともう...。困ったもんだ...(素人目線)

自分で書いているものがどちらかをしっかりと把握してからそれに合わせて書き換えましょう。
いまでも結構てこずっています

不安の塊一文コード

「このコードを追加します」という形で使える最高のコードを沢山の方が公開されています。
ただ、なれるまでは厳しかったんですよ...どこに入れていいかわからなくて...

例えば、「下記コードを◯◯.ktに追加します」って書かれていると...
「◯◯.ktのどの行にコードいれるの!?え、どこの間?一番下?え、どこぉ!!??」ってなるんですよね。
これは私だけかもしれません。でも、よく陥ってました。

実際それっぽいとこに入れてみて、ビルドも問題なくて、正常に動作するか検証するだけで分かる気もしますが、最初はかなり戸惑いました。そのような経験ありませんでしょうか?

コードを全文公開されている方は、本当にありがたかったです。ありがとうございます!!
今となっては一文でも非常にありがたいです。すべての人に感謝!!

終わりに

まだまだ自分自身学習して日が浅いので、失敗、困り事はつきものです。
その中で一個ずつ調べ、解決しながらより良いコードだったり、記事だったり、皆さんのように私も誰かの役に立てるように頑張っていこうと思います。
また、周りに初学者がいて、同じ状況になっている人が居ましたら是非助けてあげてほしいと思います。

初心忘るべからず!

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

【Kotlin】初心者によるAndroid開発 〜導入編〜

概要

プログラミングなんて無縁の金融機関で働いている私による初心者向け導入方法をまとめたものです。
私自身まだまだ初心者なので、これから始めようとしている人、やってみたいと思っている人でも分かりやすいように心がけてまとめて行きます。
今回はずっと興味があったAndroidアプリの開発を目指してやっていきます。
初心者の私が書くものなので、微妙かも...でも、気にしない!!

開発環境の実装

開発は統合開発環境(IDE)を利用します。
色々調べてみると、Androidの開発環境といえば【Android Studio】一択らしいので、ここは素直に従って使っていきます。公式にインストール方法がまとめてあるのでこちらを参考に導入しましょう。
https://developer.android.com/studio/install?hl=ja】

ちなみに、システム要求(PCの推奨スペック)ですが、最初はメモリ8Gあれば問題ないと思います。
実際のシステム要求については、公式を参考にしてください。ページ下部に記載されています。
https://developer.android.com/studio?hl=ja】

実際に使ってみる

スクリーンショット 2019-12-22 12.18.04.png
「インストール完了っ!!プロジェクト立てるぞ!!」と息巻いていたら、
よくわからないけど決めなければいけない項目があったのでざっくり解説していきます。

プロジェクト作成

スクリーンショット 2019-12-22 12.18.19.png
今回はEmpty Activityを選びます。これが個人的に最も利用頻度が高かったアクティビティです。
それはもちろん、空(Empty)だからこそ作り甲斐があり、汎用性が高いからです!
また、他のアクティビティはイメージ図通りのものが組み込まれている状態で、プロジェクトがスタートします。色々試してみるといいスタートが切れると思います。
何事も挑戦が大事!まずはどんどんやってみましょう!

とはいえ、当初「アクティビティってなんだ?」となっていました。
現在は【 アクティビティ = ロジックを組む場所 】という捉え方をしています。私の感覚なので、できれば自分なりにアクティビティについて調べてみてください。
正しい解釈や捉え方は是非コメントでご教授ください!
スクリーンショット 2019-12-22 22.43.46.png
アクティビティを決定すると、上記画面に移行します。
ここでは、プロジェクト名やパッケージ名、保存場所、使用言語など決めていきます。
パッケージ名は後々変えることもできるの、困ったらデフォルトや適当なもので大丈夫です。
保存場所も特に指定したい場所がなければデフォルトで問題ありません。

言語については、JavaかKotlinを選ぶことになりますので、今回はKotlinを選びます。

APIレベルについては、公式からアナウンスがありましたので今回は28としています。
今後アプリを作ってリリースするぞ!って方は特にこのあたり注意が必要かと思います。
https://developers-jp.googleblog.com/2019/03/2019-api.html】

instant appsのチェックは外します。詳しくは公式を参照していただき、簡単に言うと「インストール不要でアプリが使える」という仕組みです。
でも、折角ならインストールしてもらいたいじゃん!?なんかインストール不要って寂しいじゃん!?
https://developer.android.com/topic/google-play-instant/?hl=JA】

AndroidXは必須アイテムなのでチェックを付けましょう。便利ツールの集合体です。
ただ、少しこの部分は注意が必要で、Xの前身『Support Library』(V4,V7)で書かれた参考書や記事が結構多いです。気をつけていただければと思います。
スクリーンショット 2019-12-22 22.48.07.png
立ち上がりました!!いいですねぇ。

実際に動かしてみよう

スクリーンショット 2019-12-22 22.49.43.png
エミュレーターで問題なく動くか確認したいので、画面右上にある再生ボタンを押します。
再生ボタンの横に機種とAPIが記入されている部分が今回使用しているAndroid端末です。設定を変更することで、別の機種で確認することもできますので、試してみてください。
その際は実装しているAPIと機種のAPIを揃えるとエラーなくできるかと思います。

スクリーンショット 2019-12-24 0.26.54.png
Hello World!
無事動いていることが確認できました!文章部分を書き直して再生ボタンを押すと反映されますので、動いていることを確認しましょう。ここまで来るとちょっと感動します。

最後に

初めて記事を投稿しました。まだ駆け出しの初心者なので、ミスだったり誤認があったりすると思います。その場合は、お手数ですがご指摘をお願いいたします!
ミスを恥じずにTry&Errorでやっていこうと思います!!

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

【Android】Firebaseにユーザデータを保存する

はじめに

僕が個人開発しているAndroidアプリに『メモアプリ』というメモ帳アプリがあります。基本的にはオフラインで使えるシンプルなメモ帳アプリなのですが、機種変時のデータ移行を楽にしたいという要望もあり、Firebaseを使ってユーザデータのバックアップ作成/復元ができるようになっています。

FirebaseのAuth, Firestore, Storageを使うことで比較的容易にこの機能を組み込むことができましたが、それでも実装量はそれなりにあるため、本記事では、この機能の実装例を簡単にではありますが紹介します。

1. AndroidプロジェクトにFirebaseを追加する

以下の公式ドキュメントを参考にAndroidプロジェクトにFirebaseを追加します。
Android プロジェクトに Firebase を追加する

また、Firebaseの認証にGoogleログインを使いますので、以下の公式ドキュメントを参考にGoogleログインを統合します。
Android で Google ログインを使用して認証する

次に、Firebase Console上でCloud FirestoreとCloud Storageの初期設定を以下の公式ドキュメントを参考に済ませてください。
CloudFirestoreを使ってみる
AndroidでCloud Storageを使ってみる

2. Firebaseのセキュリティルール

Firebase Console上で以下のセキュリティルールを設定します。

Firestore

usersコレクション直下にFirebase Authで払い出されたユーザのUIDを持つドキュメントに対して、そのユーザのみがRead/Writeできる権限を設定します。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
  }
}

Storage

usersフォルダ直下にFirebase Authで払い出されたユーザ専用のフォルダを作り、そのフォルダに対してのみユーザ自身がRead/Writeできる権限を設定します。

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /users/{userId}/{allPaths=**} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
  }
}

3. クライアントの実装

主な処理の流れは以下の通りです。なお、アプリ仕様はサインインしなくても通常機能は使えるようになっており、バックアップ機能を有効にしたときに初めてサインインする仕様になっています。

バックアップ作成時

  1. バックアップボタンが押される
  2. 未サインインの場合はサインイン処理
  3. ユーザファイルをzipファイルに固める
  4. Storageにzipファイルをアップロード
  5. Firestoreにユーザデータを格納
  6. 成功/エラーを返す

バックアップ復元時

  1. リストアボタンが押される
  2. 未サインインの場合はサインイン処理
  3. Firestoreからユーザデータの取得
  4. Storageからユーザファイルをダウンロード
  5. zip解凍しローカルストレージに保存
  6. 成功/エラーを返す

バックアップ作成時

Firebase認証

まずは、サインイン済みかどうか判定し、未サインインならば、Googleアカウントでサインインしてもらう処理を書きます。

SettingsActivity.kt
companion object {
    private const val REQUEST_CODE_SIGN_IN_WITH_MAKE_BACKUP = 1
}

private lateinit var auth: FirebaseAuth
private lateinit var googleSignInClient: GoogleSignInClient

private val isSignedIn: Boolean
    get() = auth.currentUser != null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Firebase Authの初期化
    auth = FirebaseAuth.getInstance()

    // Google SignInの初期化
    val googleSignInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
        .requestIdToken(getString(R.string.default_web_client_id))
        .requestEmail()
        .build()
    googleSignInClient = GoogleSignIn.getClient(this, googleSignInOptions)

    // バックアップボタンをクリック
    backupButton.setOnClickListener {
        if (!isSignedIn) {
            // 未サインインならGoogleログインするダイアログを表示
            val signInIntent = googleSignInClient.signInIntent
            startActivityForResult(signInIntent, REQUEST_CODE_SIGN_IN_WITH_MAKE_BACKUP)
        }
        else {
            // バックアップ作成
            makeBackup()
        }
    }
}

private fun makeBackup() {
    // TODO: バックアップ作成
}

Googleログイン処理が完了するとonActivityResultメソッドが呼ばれます。Firebase認証後、auth.currentUserで認証ユーザ情報(ここではユーザID:uid)を取得できます。
なお、RxJavaを使っているところがありますが、本筋ではないため、説明は割愛します。

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

    if (requestCode == REQUEST_CODE_SIGN_IN_WITH_MAKE_BACKUP) {
        val task = GoogleSignIn.getSignedInAccountFromIntent(data)
        try {
            // Google Sign In was successful, authenticate with Firebase
            val account = task.getResult(ApiException::class.java)
            firebaseAuthWithGoogle(account!!)
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe { /*progressView.show()*/ }
                .doFinally { /*progressView.dismiss()*/ }
                .subscribe(object : SingleObserver<FirebaseUser> {
                    override fun onSubscribe(d: Disposable) {
                    }

                    override fun onSuccess(user: FirebaseUser) {
                        if (requestCode == REQUEST_CODE_SIGN_IN_WITH_MAKE_BACKUP) {
                            // バックアップ作成
                            makeBackup()
                        }
                    }

                    override fun onError(error: Throwable) {
                        // TODO: Error Handling
                    }
                })
        }
        catch (e: ApiException) {
            // TODO: Error Handling
        }
    }
}

// Firebase認証
private fun firebaseAuthWithGoogle(account: GoogleSignInAccount): Single<FirebaseUser> {
    return Single.create { emitter ->
        val credential = GoogleAuthProvider.getCredential(account.idToken, null)
        auth.signInWithCredential(credential).addOnCompleteListener(this) { task ->
            if (task.isSuccessful) {
                val user = auth.currentUser
                emitter.onSuccess(user!!)
            }
            else {
                emitter.onError(Error("認証エラー"))
            }
        }
    }
}

バックアップデータの作成

バックアップデータを作成します。最初にユーザの画像ファイルなどをStorageにアップロードした後、Firestoreにユーザデータを格納しています。両方の処理が成功したときのみバックアップ成功とし、それ以外はエラーを返します。

SettingsActivity.kt
private fun makeBackup() {
    auth.currentUser?.let { user ->
        val backupManager = BackupManager(this@SettingsActivity)
        backupManager.backupToStorage(user)
            .flatMap { backupManager.backupToDatabase(user) }
            .observeOn(AndroidSchedulers.mainThread())
            .doOnSubscribe { /*progressView.show()*/ }
            .doFinally { /*progressView.dismiss()*/ }
            .subscribe(object : Observer<Boolean> {
                override fun onSubscribe(d: Disposable) {
                }

                override fun onNext(t: Boolean) {
                }

                override fun onComplete() {
                    // TODO: Success Handling
                }

                override fun onError(error: Throwable) {
                    // TODO: Error Handling
                }
            })
    }
}

BackupManagerクラスは以下の通りです。Storageにusers/{userId}/backup.zipとしてファイルアップロードします。あくまでバックアップ用途のため、個別にファイル取得をすることはないので、クライアント側でzipファイルに固めてしまっています。ここでは、Zip4jを使ってzipファイルを作成しています。

Firestoreにはusers/{userId}ドキュメントにタイムスタンプとユーザデータを格納しています。

BackupManager.kt
class BackupManager(private val context: Context) {

    // Storageにファイルをアップロード
    fun backupToStorage(user: FirebaseUser): Observable<FirebaseUser> {
        return Observable.create { emitter ->
            val storage = FirebaseStorage.getInstance()
            val file = archive()
            val ref = storage.reference.child("users/${user.uid}/backup.zip")
            ref.putFile(Uri.fromFile(file))
                .addOnSuccessListener {
                    emitter.onNext(user)
                    emitter.onComplete()
                }
                .addOnFailureListener { exception ->
                    emitter.onError(Error("アップロード失敗"))
                }
        }
    }

    // Firestoreにデータを格納
    fun backupToDatabase(user: FirebaseUser): Observable<Boolean> {
        return Observable.create { emitter ->
            val db = FirebaseFirestore.getInstance()
            val data = hashMapOf(
                "storedAt" to Timestamp(Date()),  // 最終バックアップ時刻
                // TODO: 保存したいユーザデータを追記
            )
            db.collection("users").document(user.uid).set(data)
                .addOnSuccessListener {
                    emitter.onNext(true)
                    emitter.onComplete()
                }
                .addOnFailureListener { exception ->
                    emitter.onError(Error("データベースエラー"))
                }
        }
    }

    private fun archive(): File {
        val userFile1: File = /* ユーザの画像ファイルなどのFileオブジェクト */
        val userFile2: File = /* ユーザの画像ファイルなどのFileオブジェクト */

        // ユーザデータのFileオブジェクトをzipファイルに固める
        val zipFile = ZipFile("path/to/backup.zip")
        zipFile.addFiles(listOf(userFile1, userFile2))

        return zipFile.file
    }
}

バックアップ復元時

Firebase認証

バックアップ復元時も作成時と同様です。先ほどのコードに以下のように追加します。

SettingsActivity.kt
companion object {
    private const val REQUEST_CODE_SIGN_IN_WITH_MAKE_BACKUP = 1
    private const val REQUEST_CODE_SIGN_IN_WITH_RESTORE_BACKUP = 2
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    ...

    // リストアボタンをクリック
    restoreButton.setOnClickListener {
        if (!isSignedIn) {
            // 未サインインならGoogleログインするダイアログを表示
            val signInIntent = googleSignInClient.signInIntent
            startActivityForResult(signInIntent, REQUEST_CODE_SIGN_IN_WITH_RESTORE_BACKUP)
        }
        else {
            // バックアップ復元
            restoreBackup()
        }
    }
}

private fun restoreBackup() {
    // TODO: バックアップ復元
}

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

    if (requestCode == REQUEST_CODE_SIGN_IN_WITH_MAKE_BACKUP ||
        requestCode == REQUEST_CODE_SIGN_IN_WITH_RESTORE_BACKUP) {
        val task = GoogleSignIn.getSignedInAccountFromIntent(data)
        try {
            // Google Sign In was successful, authenticate with Firebase
            val account = task.getResult(ApiException::class.java)
            firebaseAuthWithGoogle(account!!)
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe { /*progressView.show()*/ }
                .doFinally { /*progressView.dismiss()*/ }
                .subscribe(object : SingleObserver<FirebaseUser> {
                    override fun onSubscribe(d: Disposable) {
                    }

                    override fun onSuccess(user: FirebaseUser) {
                        if (requestCode == REQUEST_CODE_SIGN_IN_WITH_MAKE_BACKUP) {
                            // バックアップ作成
                            makeBackup()
                        }
                        else if (requestCode == REQUEST_CODE_SIGN_IN_WITH_RESTORE_BACKUP) {
                            // バックアップ復元
                            restoreBackup()
                        }
                    }

                    override fun onError(error: Throwable) {
                        // TODO: Error Handling
                    }
                })
        }
        catch (e: ApiException) {
            // TODO: Error Handling
        }
    }
}

バックアップデータから復元

バックアップデータから復元します。Firestoreからユーザデータをロードした後、Storageからユーザの画像ファイルなどをダウンロードします。両方の処理が成功したときのみバックアップ成功とし、それ以外はエラーを返します。

SettingsActivity.kt
private fun restoreBackup() {
    auth.currentUser?.let { user ->
        val backupManager = BackupManager(this@SettingsActivity)
        backupManager.restoreFromDatabase(user)
            .flatMap { backupManager.restoreFromStorage(user) }
            .observeOn(AndroidSchedulers.mainThread())
            .doOnSubscribe { /*progressView.show()*/ }
            .doFinally { /*progressView.dismiss()*/ }
            .subscribe(object : Observer<Boolean> {
                override fun onSubscribe(d: Disposable) {
                }

                override fun onNext(t: Boolean) {
                }

                override fun onComplete() {
                    // TODO: Success Handling
                }

                override fun onError(error: Throwable) {
                    // TODO: Error Handling
                }
            })
        }
    }
}

先ほどのBackupManagerクラスに以下のコードを追加します。バックアップ作成時と逆の処理を書いていく感じです。

BackupManager.kt
class BackupManager(private val context: Context) {

    ...

    fun restoreFromDatabase(user: FirebaseUser): Observable<FirebaseUser> {
        return Observable.create { emitter ->
            val db = FirebaseFirestore.getInstance()
            db.collection("users").document(user.uid).get(Source.SERVER) // オフラインキャッシュを利用しない
                .addOnSuccessListener { document ->
                    if (document != null && document.data != null) {
                        val data: Map<String, Any> = document.data!!
                        // TODO: ユーザデータをローカルDBに保存
                        emitter.onNext(user)
                        emitter.onComplete()
                    }
                    else {
                        emitter.onError(Error("バックアップデータがありません"))
                    }
                }
                .addOnFailureListener { exception ->
                    emitter.onError(Error("データベースエラー"))
                }
        }
    }

    fun restoreFromStorage(user: FirebaseUser): Observable<Boolean> {
        return Observable.create { emitter ->
            val tmpFile = File("path/to/backup.zip")
            val storage = FirebaseStorage.getInstance()
            val ref = storage.reference.child("users/${user.uid}/backup.zip")
            ref.getFile(tmpFile)
                .addOnSuccessListener {
                    try {
                        unarchive(tmpFile)
                        emitter.onNext(true)
                        emitter.onComplete()
                    }
                    catch (e: Exception) {
                        emitter.onError(Error("復元に失敗"))
                    }
                }
                .addOnFailureListener { exception ->
                    emitter.onError(Error("復元に失敗"))
                }
        }
    }

    private fun unarchive(file: File) {
        // TODO: ローカルストレージに展開
        ZipFile(file).extractAll("path/to/")
    }
}

まとめ

FirebaseのAuth, Firestore, Storageを使い、ユーザデータのバックアップを作成する例を紹介しました。サンプルコードのままでは、データが平文で保存されるため、実際は暗号化して保存するなどした方が良いかもしれません。

参考

1) Android プロジェクトに Firebase を追加する
https://firebase.google.com/docs/android/setup?authuser=1
2) RxJava
https://github.com/ReactiveX/RxJava
3) Zip4j
https://github.com/srikanth-lingala/zip4j

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