20200707のAndroidに関する記事は4件です。

【動画解説付き】初心者のためのAndroidアプリ開発方法 ~この記事だけ見ればOK~【Android Studio/Kotlin/Windows 10】

サムネイル.png
どうも、プログラミングの鬼シヨツ鬼です。
この記事は「Androidアプリ開発始めたいぜ!だけど初心者だぜ」って人に向けて、この記事だけ見れば、開発に必要なソフトのダウンロードから、サンプルアプリを作って実機で動作させるまでができるチュートリアル記事です。
初めてアプリ作る人は、この記事を参考にアプリ作りのはじめの一歩を踏み出しちゃってください!

ちなみに、この記事は次の動画と同じ内容ですので、記事だけだと分かりづらい部分は、そちらもご活用ください。
(ボタンの位置や操作方法などがわかるので、初心者の方は動画のほうが分かりやすいかも)
【最短最速】初心者のためのAndroidアプリ開発環境構築方法 ~Android Studioのインストールから実機動作確認方法まで~

使用する環境

【開発環境PC】
 Windows10 64bit版
【Android実機】
 Rakuten Mini(Andorid 9)

Andorid Studioのインストール

Androidアプリを作るためにはAndroid Studioと呼ばれるソフトを使います。
まずはそのインストール方法を説明します。

インストーラダウンロード

下記のサイトより、インストーラをダウンロードします。
Windows用のインストーラ

Linux・Macの人はこちらの記事よりインストールできるはずです。
Android Studioのインストール

インストーラ実行

ダウンロードしたインストーラを実行すると、Android Studioのインストールが始まります。
オプションを選択するように表示されるので、設定していきます。
この記事では、重要なオプションをピックアップして紹介します。
そのほかのオプションについては動画をご覧ください。

Choose Componetsの設定

Android Virtual Deviceにチェックがついていることを確認してNextを押します。
チェックを入れることにより、仮想スマホでの検証環境を一緒に入れることができます。
image.png

Android Studio初起動時の設定

インストールが完了すると、Android Studioが起動します。
初起動時にはいくつか設定する項目があるので、重要な設定を紹介します。

Import Android Studio Settings Formの設定

こちらの画面では、上にチェックを入れると以前にAndroid Studioをインストールしたことがある場合、その設定を引き継ぐことができます。初めてのインストールであれば、下にチェックをつけたまま「OK」を押します。
image.png

Install Typeの設定

Install Typeの設定画面では、特にこだわりがなければ「Standard」を選べばOKです。

サンプルアプリ作成#1

アプリ作成画面を開く

初回起動が終わったら、次の手順でアプリ作成画面を開きます。
1.Welcome to Android Studio画面で「Start a new Android Studio project」を選ぶ
2.Select a Project Template画面で「Empty Activity」を選んでNext
3.Configure Your Project画面でNameにかっこいいアプリ名を入れてFinish
4.アプリ作成画面が開くので、完全に立ち上がるまで待つ(画面下のぐるぐるが消えるまでちゃんと待つことが重要)

「見た目」を作る

Androidアプリは「見た目」と「動き」を作ることで、一つのアプリが完成します。
まずは、「見た目」を作る練習としてボタンを配置してみます。

1.見た目を制御しているプログラムは(最初の状態では)「activity_main.xml」。「activity_main.xml」をクリックして、スマホの画面をイメージした絵が現れることを確認する。
2.左側のメニューよりボタンをドラッグアンドドロップで配置。
3.この状態で配置しただけだと、正確な位置が指定できていないため赤いびっくりマークが出てエラー扱いになるので、右側のAttributesのLayoutより正確な位置を指定。
image.png

Virtual Deviceで動かしてみる

PC上にVirtual Device(仮想スマホ)を作ってアプリの動作を検証してみます。

Virtual Deviceを作る

1.画面右上のNo Deviceより、「Open AVD Manager」をクリック
2.Your Virtual Devices画面で「Create Virtual Device」をクリック
3.Select Hardware画面で検証したいスマホの種類を選ぶ。(僕が持っているRakuten Miniはなかったので、今回は「Pixcel 2」を選びました)
4.Select a system image画面で検証したいAndroid OSのバージョンを選ぶ。(僕はRakuten Miniに合わせてAndroid9.0を選択しました)
5.ライセンスに同意する

Virtual Deviceでアプリを動かす

右上の▷ボタンを押してください。
PCの画面上にスマホが現れ、作ったアプリが起動するはずです。

image.png

サンプルアプリを作る#2

「動き」を作る

先ほど作ったアプリに表示されている「Hello World」の文字を変えてみます。

1.動きを制御しているプログラムは(最初の状態では)「MainActivity.kt」。「MainActivity.kt」をクリックして、ソースコードが表示されることを確認する。
2.下の写真のようにプログラムを書く。

image.png

実機で動かしてみる

実機側での準備

お手持ちのAndroid機で作ったアプリを動かせるように準備をします。
1.設定より、端末情報>ビルド番号を探す
2.ビルド番号を連打する
3.スマホのパスワードを入力する
4.設定より、システム>開発者向けオプションを探す
5.開発者向けオプションがONになっていることを確認する
6.USBデバッグをONにする

コードをつなぐ

PCとスマホをUSBケーブルでつなぎます。
スマホの画面に「USBデバッグを許可しますか?」と出てきたら「OK」を押す

PC側の操作

1.Virtual Deviceを作った際に、仮想スマホ名が表示されていた個所に、接続したスマホ名が表示されていることを確認
2.▷を押す
image.png

まとめ

作ったアプリが動くって楽しいね。
最後まで、読んでくれてありがとう。
参考になったら「:thumbsup_tone2:」押してね

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

【Kotlin】YouTubeAPIを用いて動画再生まで行う

はじめに

 この記事では、youtubeAPIを利用してyoutube動画を再生する方法の基本の基本を書き記しています。このAPIを使うには、GoogleAPI keyが必要になるので各自で準備をお願いします。(入手は無料で簡単です。)

準備

 youtubeAPIをAndroid Studioに実装する必要があります。以下のリンクから、YouTube Android Player APIの最新バージョンをダウンロードしてください。
https://developers.google.com/youtube/android/player/downloads/?hl=ja
 ダウンロードしたzipファイルを解凍し、android studioのlibsフォルダーに移します。ProjectタブのProjectビューからではないとアクセスしにくいので、注意してください。(Androidビューからでは✖)ポップアップはOKを押して、以下の画像のようになったらOKです。

キャプチャ.PNG
 次に、File>Project Structure>Dependencies>appを開いて、+ボタンからjar dependencyを選び、youtub APIを選択して、実装を行います。(この操作は不要かもしれませんが、一応記しておきます。)上手くいくと下のようになります。
キャプチャ.PNG
 画面最下部にyoutube APIがあることが確認できると思います。これで、ライブラリの実装は完了しました。
 また、youtubeAPIはエミュレーターにyoutubeがインストールされていないと使えないので、googlePlayにアクセスできるバージョンで開発する必要があります。

YoutubeActivityを作る

今回は、取り敢えず任意のyoutube動画の再生までを行いたいと思います。はじめに、再生したいyoutube動画のリンクを用意してください。プログラムには、リンクの?v=以後の文字列を使います。このユニークな文字列によって、APIが動画を特定し出力してくれます。
例:www.youtube.com/watch?v=dEhZzEJrcUEというリンクでは”dEhZzEJrcUE”を使う。
 あとは、コードを記述するだけです。下記にサンプルプログラムを記しておきました。今回は動画再生を行うActivityをYoutubeActivityとしています。(manifestファイルにて、インターネットのpermissionを記述し忘れないでください)

const val YOUTUBE_VIDEO_ID = "先述した任意の動画の文字列"
class YoutubeActivity : YouTubeBaseActivity(), YouTubePlayer.OnInitializedListener {
    private val TAG = "YoutubeActivity"

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

        val layout = layoutInflater.inflate(R.layout.activity_youtube, null) as ConstraintLayout
        setContentView(layout)

        val playerView = YouTubePlayerView(this)
        playerView.layoutParams = ConstraintLayout.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
        layout.addView(playerView)

        playerView.initialize(GoogleAPIkey, this)
    }

    override fun onInitializationSuccess(provider: YouTubePlayer.Provider?, youTubePlayer: YouTubePlayer?,
                                         wasRestored: Boolean) {
        Log.d(TAG, "onInitializationSuccess: provider is ${provider?.javaClass}")
        Log.d(TAG, "onInitializationSuccess: youTubePlayer is ${youTubePlayer?.javaClass}")
        Toast.makeText(this, "Initialized Youtube Player successfully", Toast.LENGTH_SHORT).show()

        if (!wasRestored) {
            youTubePlayer?.cueVideo(YOUTUBE_VIDEO_ID)
        }
    }

    override fun onInitializationFailure(provider: YouTubePlayer.Provider?,
                                         youTubeInitializationResult: YouTubeInitializationResult?) {
        val REQUEST_CODE = 0

        if (youTubeInitializationResult?.isUserRecoverableError == true) {
            youTubeInitializationResult.getErrorDialog(this, REQUEST_CODE).show()
        } else {
            val errorMessage = "There was an error initializing the YoutubePlayer ($youTubeInitializationResult)"
            Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show()
        }
    }
}

 上記のコードを記述して、YoutubeActicvityをエミュレータ上で動かすと指定した動画が再生されることが分かると思います。ここからは大まかに分かれた各ブロックについて説明していきます。

class YoutubeActivity : YouTubeBaseActivity(), YouTubePlayer.OnInitializedListener {

 まず、youtube動画を再生させるActivityにはYouTubeBaseActivity()というクラスの継承と、YouTubePlayer.OnInitializedListener()というインターフェースの実装が必要です。前者は、 YouTubePlayerViewというyoutube動画を再生されるのに必要なviewを組み込むために必要で、YouTubePlayer.OnInitializedListener()はAPIキーを認証して動画再生させるのに必要な初期化という過程を行うのに必要となります。

 val playerView = YouTubePlayerView(this)
        playerView.layoutParams = ConstraintLayout.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
        layout.addView(playerView)

 次にこの部分についてです。この部分で、先述したyoutube動画を再生するviewであるYouTubePlayerViewをレイアウトに動的に追加しています。レイアウトファイルからは直接追加が出来ないので、この方法を取っています。

playerView.initialize(GoogleAPIkey, this)

 この箇所で、YouTubePlayerViewの初期化を行っています。準備したAPIキーを使って認証を行い、動画再生を行います。この処理が上手くいった場合と行かなかった場合に分岐して、下に記してあるメソッドが実行されます。

override fun onInitializationSuccess(provider: YouTubePlayer.Provider?, youTubePlayer: YouTubePlayer?,
                                         wasRestored: Boolean) {
        Log.d(TAG, "onInitializationSuccess: provider is ${provider?.javaClass}")
        Log.d(TAG, "onInitializationSuccess: youTubePlayer is ${youTubePlayer?.javaClass}")
        Toast.makeText(this, "Initialized Youtube Player successfully", Toast.LENGTH_SHORT).show()

        if (!wasRestored) {
            youTubePlayer?.cueVideo(YOUTUBE_VIDEO_ID)
        }
    }

これは、インターフェースYouTubePlayer.OnInitializedListener()に指定されたメソッドの一つで、先述した初期化が成功した時の処理を行います。今回は、ログとトーストで正しく実行されたことを分かりやすく表示させる処理を最初に書いています。その後のif文で、wasRestoredというパラメータで条件分岐させています。ここでwasRestoredは

wasRestored : YouTubePlayerView または YouTubePlayerFragment が状態を復元する処理の一部として、プレーヤーが以前に保存された状態から復元したかどうかを示します。 通常、true はユーザーが予期した状態から再生が復元しており、新しい動画が読み込まれないことを意味します。

と、公式のリファレンスにあるのでfalseの時だけ動画の再生を促すために、youTubePlayer?.cueVideo(YOUTUBE_VIDEO_ID)を使って動画再生を行います。引数は初めに準備するようお願いした、任意のyoutube動画リンク末尾の文字列です。
 YouTubePlayerに続くcueVideoメソッドは動画のサムネイルだけ表示して、ユーザーがタップした時にはじめて再生をさせる機能を持ちます。自動的に再生をさせたいならloadVideo()を使ってください。このように、動画再生だけでも様々な機能の幅があるので、別の記事で紹介出来たらしようと思います。詳しくは公式リファレンスを参照してください。

override fun onInitializationFailure(provider: YouTubePlayer.Provider?,
                                         youTubeInitializationResult: YouTubeInitializationResult?) {
        val REQUEST_CODE = 0

        if (youTubeInitializationResult?.isUserRecoverableError == true) {
            youTubeInitializationResult.getErrorDialog(this, REQUEST_CODE).show()
        } else {
            val errorMessage = "There was an error initializing the YoutubePlayer ($youTubeInitializationResult)"
            Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show()
        }
    }

最後にonInitializationFailureを実装します。これもインターフェースによって実装が義務付けられているメソッドで、インターネットのトラブルや端末のトラブルで、初期化に失敗した場合に実行される処理です。今回は実行されたことをトーストに表示させてあげるための処理だけ書いてあります。
 以上で細かい点の解説を含め終わりです。次は、動画再生の細かい設定や別Activity再生させる方法などを書きたいと思います。アプリ開発初心者なので、細かい用語の使い方や認識が誤っている場合があるので、ぜひその時はコメントを下さい!

参考文献

YouTubeAPI公式リファレンス
https://developers.google.com/youtube/android/player/reference/com/google/android/youtube/player/package-summary?hl=ja

 

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

【Flutter】GoogleDriveへのバックアップ・リストア機能を実装するまでの道のり

トップ2.jpg

7月にFlutter開発を始めてから2作目のメモアプリ「IdeaShuffleMemo」をリリースしました。
■AppStore
https://apps.apple.com/jp/app/id1517535550

■Google Play
https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja

■アプリの詳細記事
https://yukio.site/idea_shuffle_memo/

3つの機能を実装しようと取り組みました。この記事では、その中のバックアップ機能を実装する方法を書いていきたいと思います。

■パスワードロック機能実装ついてはこちら
https://qiita.com/YuKiO-OO/items/bf2d1d107d1a66211619

■アプリ内課金についてはこちら
https://qiita.com/YuKiO-OO/items/a0fe8e0a256afbb69fc7

実装する機能

ユーザーがGoogleアカウントでログイン。
バックアップボタンを押すと、ユーザーのGoogleドライブにDBの情報を保存。
リストアボタンを押すと、バックアップしたデータが復元される。

Googleドライブ採用の理由

とりあえずでもGoogleアカウントを持っている人が多いと考えたからです。特に全世界のスマホシェアではAndroidが多く、AndroidユーザーはすべてGoogleアカウント持っていますしね。
その他ストレージはユーザーが限定されるので、選択肢に入れませんでした。
Firebaseを使って、こちら側でストレージを用意してあげる方法もありますが、サービスの維持、仮にサービスを止めることになった場合に、データの扱いが難しいことや、ユーザーデータの管理の観点でもリスクがあるので、今回は採用しませんでした。

参考記事

この記事を参考に構築しました。

GoogleAPI利用の設定:参考
Google Firebase Email/Password And Google Login In Flutter
Googleドライブの実装:参考
https://www.c-sharpcorner.com/article/google-drive-integration-in-flutter-upload-download-list-files/

特に2番目の記事は、部分部分を抜き出しているだけなので、分かりづらいと思います。
こちらの構成をみて、全体像を把握しておくことをオススメします。
Github
https://github.com/myvsparth/flutter_gdrive/blob/master/lib/main.dart

今回はこの記事を元にして書いていきますので、上記、記事を教科書だと思ってください。

この記事と異なる点は、データの取り扱い方です。

教科書では画像ファイルの管理が目的なので、ユーザーが保存データのリストからデータを選んでダウンロードする機能が実装されています。

ただバックアップ機能では、ユーザーにファイルを触らせないので、今回の実装ではバックグランドでファイルの選択をしていきます。

ユーザーは指定された操作のボタンを押すだけになります。

全体の流れ

今回は、参考記事を中心に、実装について詰まったところにフォーカスして書いていきます。

-FirebaseとGoogleドライブのAPIの設定
-Googleアカウントでログイン
-DBのデータを抜き出して、Googleドライブに保存
-Googleドライブに保存されているリストを取得
-保存されているデータをダウンロード、DBを上書き

知っておいたほうがいい知識

Googleドライブの仕様について

今回、Googleドライブを使用しますが、今回の方法は普通にGoogleドライブに保存するわけではありません。

アプリ専用の隠しフォルダが作られます。

その隠しフォルダにデータ保存するのですが、保存したデータのリストも同時に生成されます。
リストにはファイル名は、ファイル固有のIDなどが記載されています。
そのリストのIDからデータをダウンロードしたりします。
ファイル名を指定して、ダウンロードするわけでないので、ご注意ください。

またユーザーはファイルを見る事も、触ることもできません。

DBについて

今回は、DBの操作についてパッケージのMoorを使用しています。
バックアップ実装では、DBの設定が完了しているものします。。。。と初心者の方は、言われても困りますよね。

時間はかかりますが書こうと思うので、取り急ぎ下記の公式ドキュメントに挑戦してみてください。
公式ドキュメント通りにやれば、できるはず。
あなたならできる!
何やっているか、分からないところもあるかもしれませんが、それも学びだったりしますし 汗

https://moor.simonbinder.eu/docs/getting-started/
この方が書いている記事を参考にすれば行けるかなと思います。
https://qiita.com/niusounds/items/e4d731af58201ad5fe6f
Youtubeにもやり方がありました。
https://www.youtube.com/watch?v=zpWsedYMczM&t=387s

他のパッケージでDBを構築されている方は、DBのデータが保存されているディレクトリのパスとファイル名がわかれば、大丈夫だと思います。(だいたい同じところかな?)

注意事項

※20年6月時点での情報をもとに作成しています。
※試行錯誤の結果、まだリファクタリング等できていません。処理が冗長的なところや一部無駄な処理もありますので、ご了承ください。

※iOS側の設定の記述が少ない理由ですが、資料があまり残ってませんでした。おそらくiOSでは、そこまでハマりポイントがなかったと記憶しています。エラー表示を解決するだけで、問題なく進めただけかもしれないですが・・・。記述少なめですが、ご了承ください。

もし、分かりづらいところがあれば遠慮なくコメントなどに残して頂けると助かります。
みなさんと一緒に、Flutterを学習している人のためになる記事を作っていければと思います。

バックアップの実装

firebaseの設定

まず、教科書はこれを使います。
Google Firebase Email/Password And Google Login In Flutter

Firebaseとは、難しいことをしなくても、オンラインでサーバーと連携する必要がある機能を簡単に構築できるサービスです。

それで、Firebaseを使用する理由は、Googleアカウントのログイン状態を管理するためです。

Firebase側が、端末を識別してログインしている状態を判断してくれるので、楽なんです。

今回使うFirebaseのAuthenticationという機能は無料で使えるのですが、月1万(たぶん回数、単位がない)を超えると従量課金にする必要があるようです。
Firebase Pricing

ちなみにFirebase以外に、GoogleDriveのAPIを使うために「google cloud platform」、通称GCPも使っていきます。GoogleのAPIの中には有料なものがありますが、GoogleDriveは無料のようです。

教科書ではEmailの登録をしていますが、Emailの登録はしないので今回はGoogleアカウントだけをオンにすれば大丈夫です。

Emailの実装に関しては飛ばしてください。

教科書ではAndroidしか触れてませんよね。

ただ私の場合、他の機能でもFirebaseと連携するので、iOS側でもFirebaseに登録しています。(必須かは不明)

実装はこちらを参考にしてください。
https://firebase.google.com/docs/flutter/setup?hl=ja

Android側の設定で、追加する場所がいくつか分かりづらかったところを補足します。

Firebaseの設定途中でAndroidは「google-services.json」をダウンロードしますが、
android/app/src/のところに保存してください。

android/build.gradle
    dependencies {
        classpath com.android.tools.build:gradle:3.5.0
        classpath org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version
        classpath com.google.gms:google-services:4.3.3
   }
android/app/src/build.gradle
  dependencies {
     implementation org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version
     implementation com.google.firebase:firebase-analytics:17.2.2
  }
//下記の位置に追加することになると思います。
  apply plugin: com.google.gms.google-services

にそれぞれ記載します。

教科書では、ログインの処理まで書かれていますが、これは次のGoogleDriveの実装記事にもしっかりと書かれているので、とりあえずFirebaseの設定までで大丈夫です。

Google Drive APIの設定

ここからは、下記の教科書を使います。

Googleドライブの実装:参考
https://www.c-sharpcorner.com/article/google-drive-integration-in-flutter-upload-download-list-files/

補足でこのブログの作者のGithubを見ておくと、全体像が掴みやすいと思います。
オリジナルで画面を作りたい場合、とりあえずGithub通りに作ってから、部分部分のデザインを変更したほうが楽かもしれません。
https://github.com/myvsparth/flutter_gdrive/blob/master/lib/main.dart

実装方法は、STEPで順に説明されていますが、補足していきたいと思います。

STEP3の補足事項

さらっとAPIの設定しておいてね!と言われますが、補足だけしておきます。
firebaseを登録して、同じアカウントでGCP(Google Cloud Platfom APIのサービス)に登録すると、Firebaseのプロジェクトが紐づいています。
そのプロジェクトを選択して、下記の記事の通りすれば問題なくできると思います。

https://www.virment.com/google-drive-api-activate/

OAuth 同意画面などは設定しました。しかし、Outhクライアントをどうしても作ることができませんでした。ただ問題なく動作できているので、この操作は不要なのかもしれません。(おそらくテスト用するためのアカウントなのだと思いますが、自分のGoogleアカウントを利用して問題ありませんでした)

STEP4の補足事項

一応念のため、パッケージは最新のものをインストール。6月時点ではパッケージのバージョンによる不具合はありませんでした。

STEP5の補足

アンドロイド側で、STEP5を追加する場合、SdkVersionが対応していないというエラーが出るかもしれません。この場合minSdkVersionのバージョンをあげることで、エラーが解消されました。

android/app/src/build.gradle
    defaultConfig {
        applicationId "アプリのIDが入ります"
        minSdkVersion 21 //ここが21以上でエラーが消えました。
        targetSdkVersion 28
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

STEP6の補足

これはStatefulウィジェットの外側に記載するので、くれぐれもClass内に書かないようにご注意ください。

STEP7 ログインの補足

基本この通りに書いて行けば問題ありません。この処理ではfirebaseを使ってユーザーのログイン状態を確認しています。
通常Webであればセッション等でログイン中を確認していますが、firebaseでは端末を識別して、ログイン状態を把握しているそうです。
https://firebase.google.com/docs/auth?hl=ja

なので、一度ログインしてアプリを閉じても、ログイン状態を維持します。

STEP7 ログアウトの補足

ログアウト処理についても、そのまま使用させてもられば大丈夫です。

STEP7のその他について

この記事に書いてあるその他実装については、バックアップとは関係ないためそのまま流用できません。下記に修正した処理を記載していきます。

バックアップ機能の実装(SETP7を改良)

バックアップ機能は、全体の流れで説明した通りです。保存されたファイルにはそれぞれ固有のIDがふられていて、リストで管理されています。ここでは直接ファイルを触るというよりは、保存されているファイルのリストを元に、ファイルを管理する形になります。
まず、リストを取得するメソッドを作成します。

Googleドライブ内のアプリ専用領域から保存されているファイル情報をリストで取得

  Future<void> _listGoogleDriveFiles() async {
    var client = GoogleHttpClient(
        await googleSignInAccount.authHeaders); //承認情報を取得
    var drive = ga.DriveApi(client); //GoogleDrive APIにアクセス
//このアプリ専用のフォルダスペースから保存しているファイル情報(データじゃない)を取得。
    drive.files.list(spaces: 'appDataFolder', $fields: 'files(id, name, modifiedTime)').then((value) {
      setState(()  {
        list =   value; //ファイルのリスト情報を取得
        lastUpdateTime = list.files[0].modifiedTime.toLocal();//最新更新日付をセットします。
      });
    }); 
  }

教科書ではログイン後にボタンを押したらこの処理を呼ぶようになっていますが、このリストはバックグランドで取得できればいいので、ログインが確認できた時点で呼ぶようにしています。

つぎはアップロードですが、DB情報を保存してるディレクトリを取得してファイルをアップロードしていきます。教科書を元に変更しています。

Googleドライブへファイルをアップロード

//これは関数でclass外に書いてます。
//DBが保存されているディレクトリのパスを取得する処理
Future<String> get getDbPath async {
  final dbDir = await getApplicationDocumentsDirectory();//ファイルが保存されている領域のパスを取得
  final dbPath = join(dbDir.path, 'defult.db');//保存されているDBのファイル名を調べて、取得したパスと合体させて絶対パスにする。
  return dbPath;
}
//class外処理終了

//ここらからstatefulウィジェット内の処理
  _uploadFileToGoogleDrive() async {
//ローディング画面を表示。別の操作をされたくないので。
    showGeneralDialog(
        context: context,
        barrierDismissible: false,
        transitionDuration: Duration(seconds: 2),
        barrierColor: Colors.black.withOpacity(0.5),
        pageBuilder: (BuildContext context, Animation animation,
            Animation secondaryAnimation) {
          return Center(
            child: CircularProgressIndicator(),
          );
        }
    ); 
   //ファイルのアップロードを始めます。
    var client = GoogleHttpClient(await googleSignInAccount.authHeaders);//承認情報を取得
    var drive = ga.DriveApi(client);//APIヘアクセス
    drive.files.list(spaces: 'appDataFolder');
    ga.File fileToUpload = ga.File();//ドライブ用のファイルのインスタンスを作成
    var filePath = await getDbPath;//DBファイルのパスを関数で取得
    await _listGoogleDriveFiles();//Googleドライブのリストを取得
    //保存するファイルの加工
    var file = await File(filePath);//ファイルをセット
    fileToUpload.name = path.basename(filePath);//ファイルの名前をセット
    fileToUpload.modifiedTime = DateTime.now().toUtc();//アップロードの日付
    fileToUpload.parents = ["appDataFolder"];//アプリ専用フォルダを指定

//常に保存されるバックアップファイルは一つにしたいので、以下の処理を入れています。
//すでにファイルがある場合
//すべてのファイルを事前に削除。エラーで複数ある場合もあるので。
    if (list.files.length > 0) {
      for (var i = 0; i < list.files.length; i++) {
        ga.Media file = await drive.files //削除用
            .delete(list.files[i].id);
      }
      print('ファイルを削除しました。');
    }
    //ファイルのアップロード処理
    var response;
    response = await drive.files.create(
      fileToUpload,
      uploadMedia: ga.Media(file.openRead(), file.lengthSync()),
    );

    await _listGoogleDriveFiles();//リストの再取得
    await Future.delayed(Duration(seconds: 1));
    Navigator.pop(context);//ローディング画面を消す
    //デバック用
    print("ファイル保存が完了しました。レンスポンスは$response");
  }

ここではDBのファイルがあるディレクトリのパスを指定してから、ファイルをアップロードするために情報を加工してアップロードしています。
教科書では日付を取得していませんが、今回バックアップした日付を表示したかったので、日付も保存しています。
どのような項目があるかはこちらでチェックできます。
https://developers.google.com/drive/api/v3/reference/files
「toUtc()」で世界協定時にしているのは、googleのapiが受け付ける形式が世界協定時だからです。

Googleドライブからダウンロード

Future<void> _downloadGoogleDriveFile() async {
//処理中は触ってほしくないのでローディング画面を表示
    showGeneralDialog(
        context: context,
        barrierDismissible: false,
        transitionDuration: Duration(seconds: 1),
        barrierColor: Colors.black.withOpacity(0.5),
        pageBuilder: (BuildContext context, Animation animation,
            Animation secondaryAnimation) {
          return Center(
            child: CircularProgressIndicator(),
          );
        }
    );
//ダウンロードを開始します。
//いつもの処理がスタートします。

    var client = GoogleHttpClient(await googleSignInAccount.authHeaders);

//常にファイルは1つのなので、リストの先頭のIDで取得。
//IDはファイルそれぞれ固有の番号がふられています。
    var drive = ga.DriveApi(client);
    ga.Media file = await drive.files
        .get(list.files[0].id,
        downloadOptions: ga.DownloadOptions.FullMedia);


    final filePath = await getDbPath; //ディレクトり取得
    final saveFile = File(filePath); //ファイルの保存場所
    //ここは教科書通りに。
    List<int> dataStore = [];
    file.stream.listen((data) {
      dataStore.insertAll(dataStore.length, data);
    }, onDone: () async {
      await saveFile.writeAsBytes(dataStore);
      _listGoogleDriveFiles();//リストを再取得
      await Future.delayed(Duration(seconds: 1));
      Navigator.pop(context); //ローディングを閉じる
//バックアップをダウンロードして、保存が成功した場合の処理
    }, onError: (error) {
      //エラー処理
    });
  }

バックアップファイルは常に1つしかないので、リストの先頭のみを引っ張ってきます。
処理はアップロードと共通でDBのファイルパスをとってきて、そこに保存します。

GoogleDriveのデータを削除

ユーザー自らデータを削除したい場合もあると思うので、一応削除ボタンも作っておきます。

  Future<void> _deleteFileToGoogleDrive() async{
//実行中は触らせたくないので、ローディング画面
    showGeneralDialog(
        context: context,
        barrierDismissible: false,
        transitionDuration: Duration(seconds: 1),
        barrierColor: Colors.black.withOpacity(0.5),
        pageBuilder: (BuildContext context, Animation animation,
            Animation secondaryAnimation) {
          return Center(
            child: CircularProgressIndicator(),
          );
        }
    );
    _listGoogleDriveFiles(); //保存リストを取得。
    var client = GoogleHttpClient(await googleSignInAccount.authHeaders);
    var drive = ga.DriveApi(client); //GoogleDriveのAPIに接続
//繰り返しでリストの全ファイルを削除
    for (var i = 0; i < list.files.length; i++) {
      ga.Media file = await drive.files 
          .delete(list.files[i].id);
    }
    await _listGoogleDriveFiles();//ファイルを再取得
    setState(() {
      lastUpdateTime = null; //表示用更新日付の空に
    });
    await Future.delayed(Duration(seconds: 1));
    Navigator.pop(context); //ローディング画面を閉じる

   //削除完了後の処理を書く

}

基本的にリストで取得して、その中にあるファイルをすべて繰り返し処理で削除するようにしています。

まとめ

いかがでしたか?
バックアップの概要がわかるまでは意味不明かと思いますが、どのように動作しているかわかってくれば、そこまで難しいものではないと思います。(改めてコードを見返してみると、リファクタリングしないとなと実感してます笑)

分からないところがあれば、コメントください。

どのように動作しているかは、アプリをダウンロードしてチェックしてみてください。
■AppStore
https://apps.apple.com/jp/app/id1517535550

■Google Play
https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja

またツイッターでもFlutterや個人開発についていろいろ呟いています。
チェックください!

https://twitter.com/oo_forward

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

android-samplesのdev-hiltブランチのHiltの実装を読んでみる

Dagger Hiltを使った場合に、まだどのように実装していくのが良いのかよくわかっっていない部分があるので、公式の実装に近そうな、android/architecture-samplesのdev-hiltブランチを読んでいきます。
Dagger Hiltのサンプルとして実用的に見えるものがここに上がっているようなので、参考に見ていきます。
https://github.com/android/architecture-samples/tree/dev-hilt
以下時点のコードになります。
https://github.com/android/architecture-samples/tree/f2fd9ce969a431b20218f3ace38bbb95fd4d1151

Dagger Hiltは以下のようなコンポーネントが最初から用意されています。それぞれのコンポーネントにどのようにインスタンスがひもづいているのか見ていきましょう。
image.png
https://developer.android.com/training/dependency-injection/hilt-android より

ApplicationComponent

@HiltAndroidAppのアノテーションを付与することで、AppComponentにApplicationクラスが紐付けされます。

@HiltAndroidApp
class TodoApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) Timber.plant(DebugTree())
    }
}

またApplicationComponentにAppModuleとTasksRepositoryModuleが紐付けされています。
ApplicationComponent
- AppModule
- TasksRepositoryModule

@Module
@InstallIn(ApplicationComponent::class)
object AppModule {
...
}

/**
 * The binding for TasksRepository is on its own module so that we can replace it easily in tests.
 */
@Module
@InstallIn(ApplicationComponent::class)
object TasksRepositoryModule {
...
}

AppModuleではTasksLocalDataSourceなどのインスタンスを配布していますが、TasksDataSourceというインターフェースの型で配布しており、RemoteTasksDataSourceなのかLocalTasksDataSourceなのかの識別できるようにするため、Qualifierのアノテーションを付けています。

@Module
@InstallIn(ApplicationComponent::class)
object AppModule {

    @Qualifier
    @Retention(RUNTIME)
    annotation class RemoteTasksDataSource

    @Qualifier
    @Retention(RUNTIME)
    annotation class LocalTasksDataSource
...

    @Singleton
    @LocalTasksDataSource
    @Provides
    fun provideTasksLocalDataSource(
        database: ToDoDatabase,
        ioDispatcher: CoroutineDispatcher
    ): TasksDataSource {
        return TasksLocalDataSource(
            database.taskDao(), ioDispatcher
        )
    }
...
}

そして、RemoteTasksDataSourceなどのアノテーションを使ったTasksDataSourceを使って、provideTasksRepositoryにてRepositoryを作成します。

/**
 * The binding for TasksRepository is on its own module so that we can replace it easily in tests.
 */
@Module
@InstallIn(ApplicationComponent::class)
object TasksRepositoryModule {

    @Singleton
    @Provides
    fun provideTasksRepository(
        @AppModule.RemoteTasksDataSource remoteTasksDataSource: TasksDataSource,
        @AppModule.LocalTasksDataSource localTasksDataSource: TasksDataSource,
        ioDispatcher: CoroutineDispatcher
    ): TasksRepository {
        return DefaultTasksRepository(
            remoteTasksDataSource, localTasksDataSource, ioDispatcher
        )
    }
}

ApplicationComponentで配布されているものをテストでどう切り替えるか?

以下のようなテストのソースセットでFakeを提供するモジュールを定義して、@UninstallModulesでFakeではないものを消して、入れ替えてしまうことで対応しています。

@Module
@InstallIn(ApplicationComponent::class)
abstract class TestTasksRepositoryModule {
    @Singleton
    @Binds
    abstract fun bindRepository(repo: FakeRepository): TasksRepository
}
@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
// ** ↓UninstallModulesでデフォルトのモジュールを消して、TestTasksRepositoryModuleを読み込ませる  **
@UninstallModules(TasksRepositoryModule::class)
@HiltAndroidTest
class TasksFragmentTest {

ActivityComponent

@AndroidEntryPointのアノテーションを付与することで、ActivityComponentと紐付けされます。

@AndroidEntryPoint
class TasksActivity : AppCompatActivity() {

このActiivtyでのFragmentの作り方はNavigationによるものです。

TasksActivity.kt
        val navController: NavController = findNavController(R.id.nav_host_fragment)
        appBarConfiguration =
            AppBarConfiguration.Builder(R.id.tasks_fragment_dest, R.id.statistics_fragment_dest)
                .setDrawerLayout(drawerLayout)
                .build()
        setupActionBarWithNavController(navController, appBarConfiguration)
        findViewById<NavigationView>(R.id.nav_view)
            .setupWithNavController(navController)
tasks_act.xml
        <fragment
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"

            app:defaultNavHost="true"
            app:navGraph="@navigation/nav_graph" />
nav_graph.xml
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/nav_graph"
    app:startDestination="@id/tasks_fragment_dest">
   <!-- **↑ 開始Fragmentを指定 ↑** -->

    <fragment
        android:id="@+id/task_detail_fragment_dest"
        android:name="com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetailFragment"
        android:label="Task Details">
...
        <argument
            android:name="taskId"
            app:argType="string" />
...
    </fragment>
   <!-- **↓ 開始Fragmentを指定 ↓** -->
    <fragment
        android:id="@+id/tasks_fragment_dest"
        android:name="com.example.android.architecture.blueprints.todoapp.tasks.TasksFragment"
        android:label="@string/app_name">
...
        <action
            android:id="@+id/action_tasksFragment_to_taskDetailFragment"
            app:destination="@id/task_detail_fragment_dest" />
...
    </fragment>

FragmentComponent

@AndroidEntryPointを使うことでFragmentComponentと紐付けされます。

TasksFragmentは以下のようにタスクリストを表示するものです。引数はnavigationから取得するようです。

@AndroidEntryPoint
class TasksFragment : Fragment() {
    private val viewModel by viewModels<TasksViewModel>()
    private val args: TasksFragmentArgs by navArgs()

ViewModelは @ViewModelInjectによってうまくInjectされるようです。 ( ここの仕組みは以前見てみました。 https://qiita.com/takahirom/items/36f658b01d1d121b3760 )

class TasksViewModel @ViewModelInject constructor(
    private val tasksRepository: TasksRepository,
    @Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
...

Fragmentのテストはどのように行うか?

まずは最初に実際にテストを見ていく前に、使われるモジュールなのですが、テストのソースセットには以下のようなモジュールが存在しており、これはFakeRepositoryを読み込むもので、テストでこのモジュールが使われるようになります。

@Module
@InstallIn(ApplicationComponent::class)
abstract class TestTasksRepositoryModule {
    @Singleton
    @Binds
    abstract fun bindRepository(repo: FakeRepository): TasksRepository
}

@UninstallModules(TasksRepositoryModule::class)によって通常のリポジトリが読み込まれるのを阻止します。
② HiltAndroidRule#injectにより、テストにFakeRepositoryがInjectされます。

@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
// ** ①通常のリポジトリが読み込まれるのを阻止する **
@UninstallModules(TasksRepositoryModule::class)
@HiltAndroidTest
class TasksFragmentTest {

    @get:Rule
    var hiltRule = HiltAndroidRule(this)
    // ② HiltAndroidRuleにより、FakeRepositoryがInjectされる。
    @Inject
    lateinit var repository: TasksRepository

    @Before
    fun init() {
        // Populate @Inject fields in test class
        hiltRule.inject()
    }

あとはFakeRepositoryにデータを保存し、Activityを起動して、表示されることでUIのテストをしています。

    @Test
    fun displayTask_whenRepositoryHasData() {
        // GIVEN - One task already in the repository
        repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1"))

        // WHEN - On startup
        launchActivity()

        // THEN - Verify task is displayed on screen
        onView(withText("TITLE1")).check(matches(isDisplayed()))
    }

    private fun launchActivity(): ActivityScenario<TasksActivity>? {
        val activityScenario = launch(TasksActivity::class.java)
        activityScenario.onActivity { activity ->
            // Disable animations in RecyclerView
            (activity.findViewById(R.id.tasks_list) as RecyclerView).itemAnimator = null
        }
        return activityScenario
    }

直接Fragmentを起動するテストもあるのですが、 launchFragmentInHiltContainer() という関数を定義して使っています。

    @Test
    fun clickAddTaskButton_navigateToAddEditFragment() {
        // GIVEN - On the home screen
        val navController = mock(NavController::class.java)

        launchFragmentInHiltContainer<TasksFragment>(Bundle(), R.style.AppTheme) {
            Navigation.setViewNavController(this.view!!, navController)
        }

        // WHEN - Click on the "+" button
        onView(withId(R.id.add_task_fab)).perform(click())

        // THEN - Verify that we navigate to the add screen
        verify(navController).navigate(
            TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
                null, getApplicationContext<Context>().getString(R.string.add_task)
            )
        )
    }

なぜlaunchFragmentInHiltContainer()が必要かと言うと androidx.fragment:fragment-testingEmptyFragmentActivity というActivityを使うのですが、このActivityは @AndroidEntryPointがついていないので利用できないためです。 HiltTestActivityというActiivtyを定義して、それを利用して起動するコードになります。

/**
 * launchFragmentInContainer from the androidx.fragment:fragment-testing library
 * is NOT possible to use right now as it uses a hardcoded Activity under the hood
 * (i.e. [EmptyFragmentActivity]) which is not annotated with @AndroidEntryPoint.
 *
 * As a workaround, use this function that is equivalent. It requires you to add
 * [HiltTestActivity] in the debug folder and include it in the debug AndroidManifest.xml file
 * as can be found in this project.
 */
inline fun <reified T : Fragment> launchFragmentInHiltContainer(
    fragmentArgs: Bundle? = null,
    @StyleRes themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme,
    crossinline action: Fragment.() -> Unit = {}
) {
    val startActivityIntent = Intent.makeMainActivity(
        ComponentName(
            ApplicationProvider.getApplicationContext(),
            HiltTestActivity::class.java
        )
    ).putExtra(EmptyFragmentActivity.THEME_EXTRAS_BUNDLE_KEY, themeResId)

    ActivityScenario.launch<HiltTestActivity>(startActivityIntent).onActivity { activity ->
        val fragment: Fragment = activity.supportFragmentManager.fragmentFactory.instantiate(
            Preconditions.checkNotNull(T::class.java.classLoader),
            T::class.java.name
        )
        fragment.arguments = fragmentArgs
        activity.supportFragmentManager
            .beginTransaction()
            .add(android.R.id.content, fragment, "")
            .commitNow()

        fragment.action()
    }
}

android-samplesのFragmentのテストのスコープは?

どうやらandroid-samplesではViewModelをmock化やDummyを差し込むようなことはせず、Repositoryを差し替えることで、Fragmentなどのテストを行っているようです。つまり範囲としてはFragment + ViewModelの範囲をTasksFragmentTestが見る形になっています。なのですが、TasksViewModelTestもあるので、TasksFragmentTestはUnitTestではなく、少し広い範囲を見るIntegrationTestに近い感じなのかなと思いました。
このFragmentだけ切り出してテストする場合にどのようにするのかなどは少し興味があったのですが、このサンプルでは見ることができなさそうでした。

まとめ

ほんとにあんまり紹介することがないぐらい、基本的には@AndroidEntryPointを定義することで、簡単に利用できていきそうです。
またテストでは少し課題があるものの、ワークアラウンドもあるようなので、チェックしてみると良さそうです。

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