20200401のAndroidに関する記事は5件です。

MVVM + DataBinding でHello World

はじめに

「Hello World」をDataBindingで作ったのでメモ。

目標物

Untitled Diagram (1).png

構成

Untitled Diagram.png

Viewには、値を表示するTextViewとイベント発生用のButtonのみ配置。
それぞれをViewModelの変数やメソッドとバインドさせる。
画面に表示するデータの保持、整形はModelが担う。

やったこと

1. Gradle Scriptに追記

DataBindingを有効にするために、以下のように追記する。

build.gradle
android {
...
    dataBinding {
        enabled true
    }
...
}

2. レイアウトファイルを修正

既存のレイアウトファイルをデータバインディングレイアウトに変更する。

デフォルトだとこんな感じ。

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">

</androidx.constraintlayout.widget.ConstraintLayout>

2.1. レイアウトを<layout>タグで囲む

データバインディングレイアウトはルートが<layout>でその子要素として
<ConstraintLayout><LinearLayout>を定義する。

手動でも変更できるが、Android Studioは自動で修正する機能を提供している。

現在のルート要素(レイアウト)にカーソルを置き、表示される電球マークから
「Convert to data binding layout」を選択
スクリーンショット 2020-04-01 16.11.02.png

データバインディングレイアウトに変更される。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

2.2 レイアウトファイルに部品などを置いていく

レイアウトファイルに部品(TextViewとか)を置いていく。
今回は、TextViewとButtonのみのシンプルな構成とする。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/tvHello"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="32sp"
            app:layout_constraintBottom_toTopOf="@+id/btHello"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="HELLO" />

        <Button
            android:id="@+id/btHello"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Say Hello"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tvHello" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

3. Modelを作成する

画面に表示するmessage、呼び出された回数を保持するcount
それぞれを連結して返却するgetHelloMessageWithCountメソッドを持つ単純なModel。

Hello.kt
data class Hello(
    var message: String,
    private var count: Int
) {
    fun getHelloMessageWithCount(): String {
        return "$message # ${++count}"
    }
}

4. ViewModelを作成する

MVVMアーキテクチャでは重要な役割を担うViewModelを作成する。

MainViewModel.kt
class MainViewModel: ViewModel() {
    private var mHello: Hello = Hello("Hello World", 0)
    private val _hello = MutableLiveData<String>()
    val hello: LiveData<String> = _hello

    init {
        _hello.value = mHello.message
    }

    fun onSayHelloClick() {
        _hello.value = mHello.getHelloMessageWithCount()
    }
}

MutableLiveData<String>()はAndroid アーキテクチャ コンポーネント(AAC)におけるところのLiveDataのインスタンス。
TextViewに表示する文字列(android:text)にバインドさせたいため、型引数Stringとして定義した。

onSayHelloClickはButtonからのonClickで発火するメソッド。ViewのButtonに対してバインドする。

5. レイアウトファイルの<data>を設定する

レイアウトファイルの<data>タグの中には、<variable>を用いて
バインドさせたい要素の情報を定義する。

activity_main.xml
<data>
    <variable
        name="viewmodel"
        type="com.example.mymvvm.viewmodel.MainViewModel" />
</data>

name属性にはtypeで使用するViewModelのレイアウトファイル内での
名前を記載する。
type属性には、バインドさせるViewModelを指定する。

6. ViewとViewModelをバインドさせる

View(Activity)とViewModelをバインドさせるため、Activityを修正。

MainActivity.kt
class MainActivity : AppCompatActivity() {

    private val mViewModel by lazy {
        ViewModelProvider(this).get(MainViewModel::class.java)
    }

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

        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(
            this, R.layout.activity_main
        )
        binding.viewmodel = mViewModel
        binding.lifecycleOwner = this
    }
}
private val mViewModel by lazy {
        ViewModelProvider(this).get(MainViewModel::class.java)
    }

ViewModelを保持するためのメンバ変数。
lazyを使って、実際に使われるタイミングで初期化されるようにする(遅延評価)。
その処理をbyで移譲する。

val binding = DataBindingUtil.setContentView<ActivityMainBinding>(
            this, R.layout.activity_main
        )

既存のsetContentViewメソッドを削除し、データバインディングをするために
DataBindingUtil.setContentViewメソッドに変更する。
なお、ActivityMainBindingは自動的に作成されるクラス。

binding.viewmodel = mViewModel
binding.lifecycleOwner = this

DataBindingUtil.setContentViewの返却値であるインスタンスを使って
作成したViewModelとのバインドを行う。
ここで、binding.viewmodelviewmodelは2.5で設定した
name=xxxxと一致させる。

併せて、lifecycleOwnerをthis(MainActivity)として設定する。
これは、LiveDataがライフサイクルを認識できる監視対象であり、使用するライフサイクルの所有者を指定する必要があるため。

7. レイアウトの属性値とViewModelを紐づける

レイアウトの属性値とViewModelの変数を紐づけることにより、紐づけたデータが
画面に反映される。

activity_main.xml
android:text="@{viewmodel.hello}"

android:onClick="@{() -> viewmodel.onSayHelloClick()}"

レイアウトファイルのTextViewのtext属性を
ViewModelのhelloと紐づける。
こうすることで、ViewModel側でhelloが更新されたときに、
その変更を検知し、View側に自動的に反映される。

また、ButtonのonClick属性の値として、
ラムダ式を用いてViewModelのonSayHelloClickメソッドと紐づける。
クリック時に、onSayHelloClickが呼ばれて、中の処理が実行される。

8. 動作確認

こんな感じに動作する。

ya2v4-8jmnw.gif

https://github.com/HisayoshiChiku/MyMVVM/

おわりに

指摘事項お待ちしております。

参考

Android Data Binding ⬅︎ わかりやすいので是非。

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

Flutter環境構築 - 1(Flutter SDKインストール編)

はじめに

Flutterの開発環境構築の手順について解説していきます。
今回は「Flutter SDKインストール編」です!
今後「Xcodeインストール編」「Android Studioインストール編」「VS codeインストール編」も随時執筆していく予定です。
さらにその後には「シミュレーターでの動作確認編」「アプリ実行編」なども執筆予定ですのでしばらくお待ちいただけると幸いです。
また今回の解説では、私自身がmacOSを使用しているということもあり、macOSでの手順解説となります。
Windows・Linuxを使用されている方は別途、Flutter公式サイトのWindows版Linux版 などをご参照下さい。

「Flutter SDKインストール編」でやること

本記事では、以下の項目を行います。

  • Flutter SDKをインストールする
  • Flutterコマンドが常に使えるようにする

システム要件

まずFlutterをインストールして実行するには、以下の要件を満たしている必要があります。

  • オペレーティングシステム(OS): macOS(64-bit)
  • ディスク容量: 2.8 GB (IDE /ツールのディスク容量は含まれません)
  • ツール: 以下のコマンドラインツールが使用できること。
    • bash
    • curl
    • git 2.x
    • mkdir
    • rm
    • unzip
    • which
    • zip

この条件が揃っていましたら、早速環境構築していきましょう!

Flutter SDKをインストールする

1.Flutter公式サイト右上の「Get started」をクリック

スクリーンショット 2020-04-01 15.18.47.png

2.installページにきたらmacOSを選択

スクリーンショット 2020-03-29 23.41.45.jpg

3.Get the Flutter SDK 欄のインストールボタンをクリックし、Flutter SDKをインストール

スクリーンショット 2020-03-29 23.41.56.png

4.完了したら、インストールしたzipファイルをクリックして解凍

Macでは基本「Downloads」の中にインストールされていると思います。
解凍できたら、次項目からターミナルにてコマンドを実行していきます。

5.ホームディレクトリに移動

cd

6.「development」ディレクトリを作成

mkdir development

7.作成した「development」ディレクトリに移動

cd development

8.「flutter」ファイルを「development」ディレクトリの中に移動

ユーザー名のところは各自のユーザー名を入力して下さい。
また、インストールされたflutterファイルの場所によってこちらは書き換えて下さい。

mv /Users/ユーザー名/Downloads/flutter ./

9.「flutter」ファイルに移動

cd flutter

Flutterコマンドが常に使えるようにする

1.Flutterをパスに常に追加する

vi ~/.bash_profile

「i」をタイプして挿入モードにし、export PATHの先頭に以下コードを記入します。
ユーザー名は各自ユーザー名を記述して下さい。
パスの区切りの「:」を忘れないようにして下さい。

/Users/ユーザー名/development/flutter/bin:

コードの全体(例)

export PATH="/Users/ユーザー名/development/flutter/bin:~/.rbenv/shims:/usr/local/bin:$PATH"

escキーで挿入モードを終了します。

編集が完了したら、「:wq」を入力しEnterで編集を終了させます。

:wq

2.Flutterコマンドが実行できるようにする

source ~/.bash_profile

3.Flutterコマンドを実行

flutter

するとコマンドが実行され、「Welcome to Flutter!」と表示されます。
スクリーンショット 2020-03-30 0.07.08.png

これでFlutterコマンドが実行できるようになりました!

最後に

以上で「Flutter SDKインストール編」は終了となります。お疲れ様でした。

またその次のステップとして
「Xcodeインストール編」「Android Studioインストール編」「VS codeインストール編」
の方も随時投稿していきます。よろしくお願いいたします。

最後までご覧下さりありがとうございました。

参考サイト[Flutter公式サイト]

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

Flutterで環境構築からHelloWorld & スプラッシュまでやってみた。【HelloWorld&スプラッシュ編】

お久しぶりです。
多忙な日々を送っていたらもう4月になってました。

今更感がすごいのですが前回の環境構築編からそのまま放置状態だったので
今回は簡単なスプラッシュ〜Helloworldを作成していきましょう。

画像を呼び出せるように設定する。

スプラッシュスクリーンで画像を表示しようと思うのでpubspec.yamlにて下記の設定を追加します。

pubspec.yaml
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # ↓この部分を追加する↓
  assets:
    - assets/images/logo.png

プロジェクトディレクトリ配下にassets/imagesディレクトリを作成しlogo.pngを配置してください。

main.dartを修正する。

すでにプロジェクトディレクトリ内の/libの中にmain.dartというファイルがあるかと思います。
main.dartを下記のようにしてスッキリさせませう。

main.dart
import 'package:flutter/material.dart';
import 'package:my_app/splash.dart';

void main() => runApp(MyApp());

スプラッシュ画面の作成

今回実装するスプラッシュではAppbarを利用しませんので記述しません。
3秒間スプラッシュ画面を表示してその後メインスクリーン(例ではAppScreen)に遷移するという流れです。
.thenを利用したいので非同期処理であるFutureを利用しています。

通常は画面遷移の場合Navigator.pushを利用するのですが、Navigator.pushでメインスクリーンへ遷移した場合スプラッシュ画面に戻れてしまいます。
そのため、画面を上乗せするのではなく、置き換えをするNavigator.pushReplacementを利用します。
FadeTransitionも利用していますが公式ドキュメントを読んでいただくかYoutubeの「Widget of the Week」にて説明していますのでそちらを参考にしていただければと思います。

https://api.flutter.dev/flutter/widgets/FadeTransition-class.html

splash.dart
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';
import 'package:my_app/app_screen.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'スプラッシュ',
      home: Splash(),
    );
  }
}

class Splash extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _SplashState();
}

class _SplashState extends State<Splash> with TickerProviderStateMixin {
  AnimationController _controller;
  Animation<double> _animation;
  @override
  void initState() {
    super.initState();
    Future.delayed(Duration(milliseconds: 3000)).then((value) =>
        Navigator.pushReplacement(
            context, MaterialPageRoute(builder: (context) => AppScreen())));
    _controller = AnimationController(
        duration: const Duration(milliseconds: 1000), vsync: this);
    _animation = CurvedAnimation(parent: _controller, curve: Curves.easeIn);
    _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: FadeTransition(
            opacity: _animation,
            child: Image.asset('assets/images/logo.png'),
          ),
        ),
      ),
    );
  }
}

HelloWorldを表示する為だけのスクリーンを作成する。

app_screen.dart
import 'package:flutter/material.dart';

class AppScreen extends StatefulWidget {
  static const String routeName = '/app';

  @override
  _AppScreenState createState() => _AppScreenState();
}

class _AppScreenState extends State<AppScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('ホームスクリーン'),
        ),
        body: Center(child: Text("HelloWorld")));
  }
}

設定した画像がフェードで表示された後に一方通行の画面遷移をしてあとにHelloWorldが表示されたと思います!
おめでとうございます!!!:v_tone2::v_tone2::v_tone2:

hello.png

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

[Android]ページ毎にデータを読み込む RecyclerView を作ってみる

はじめに

ページ毎にデータを読み込む RecyclerView を作ってみたいと思います。
アーキテクチャは Google が推奨している MVVM を利用して、次の手順で作成を進めます。

No タイトル
Step 0 必要なライブラリをセットアップする
Step 1 Retorift でページ毎のデータを取得する
Step 2 PagedList でページ毎のデータを管理する
Step 3 RecyclerView に PagedList のデータを表示する
Step 4 作成したクラスの動作を確認する

作成したときの完成イメージは次のような感じになります。

Image from Gyazo

TL;DR

  • PagedList がページ毎のデータの管理をする。
  • PagedList を生成するには DataSource・DataSource.Factory が必要になる。
  • PagedList を RecyclerView で表示するには PagedListAdapter が必要になる。
  • ページごとのデータ管理は PageList が中心となって行う、
    そのため PagedList の生成や表示に必要となる周辺のクラスを実装していく必要がある。
    それらの作成するクラスをまとめると次の表や図のような感じになる。
分類 名称 説明
View PagedListAdapter RecyclerView に PagedList を表示するための Adapter クラスの実装
ViewModel LiveData<PagedList<Item>> ページ毎のデータ管理を行うためのクラスの実装
Model PageKeyedDataSource PagedListを生成するのに必要となるクラスの実装
Model Data Source Factory PagedListを生成するのに必要となるクラスの実装
Model Service Class ページ毎のデータ取得するクラスの実装

image.png

Step 0 必要なライブラリをセットアップする

アプリケーションの作成に必要となる、Koin・Retrofit・Paging・RecyclerView・CardView などのライブラリをインストールする。

ライブラリ バージョン 説明
Koin 2.1.3 DI ライブラリ
Retrofit 2.2.4 HTTP クライアントライブラリ
Paging 2.1.2 Paging ライブラリ
RecyclerView 1.1.0 RecyclerView を利用するために必要
CardView 1.0.0 CardView を利用するために必要
dependencies {
    
    
    def koin_version = "2.1.3"
    implementation "org.koin:koin-android:$koin_version"
    implementation "org.koin:koin-android-scope:$koin_version"
    implementation "org.koin:koin-android-viewmodel:$koin_version"
    implementation "org.koin:koin-android-ext:$koin_version"

    def retrofit_version ="2.1.0"
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"

    def lifecycle_version = "2.2.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

    def paging_version = "2.1.2"
    implementation "androidx.paging:paging-runtime:$paging_version"

    def recycler_view_version = "1.1.0"
    implementation "androidx.recyclerview:recyclerview:$recycler_view_version"

    def card_view_version = "1.0.0"
    implementation "androidx.cardview:cardview:$card_view_version"
    
    
}

Step 1 Retorift でページ毎のデータを取得する

今回はページ単位の情報を取得する API として Qiita の記事一覧の取得 API ( GET /api/v2/items ) を利用します。HTTP Client には Retrofit を利用します。なので次のクラスを作成して API を呼び出せるようにしていきます。

役割 クラス名 役割
Entity Item Qiitaの記事データを表すデータクラス
Service QiitaService Qiita API を利用するためのサービスクラス
Data Source ItemDataSource QiitaService を利用してページ毎のデータを取得するクラス
Data Source Factory ItemDataSourceFactory ItemDataSource を生成するクラス

Item
Qiitaの記事データを表すデータクラスを宣言します。Item には Group や Tag や User といった情報も含まれるのであわせて定義していきます。API で利用するデータクラスはJSON To Kotlin Classを利用すると JSON を入力するだけで自動生成してくれるので楽です。

data class Item(
    val body: String, val editing: Boolean, val comments_count: Int, val created_at: String,
    val group: Group, val id: String, val likes_count: Int, val page_views_count: Int, val `private`: Boolean,
    val reactions_count: Int, val rendered_body: String, val tags: List<Tag>, val title: String, val updated_at: String,
    val url: String, val user: User
)

data class Group(
    val created_at: String, val id: Int, val name: String,
    val `private`: Boolean, val updated_at: String, val url_name: String
)

data class Tag(val name: String, val versions: List<String>)

data class User(
    val description: String, val facebook_id: String, val followees_count: Int,
    val followers_count: Int, val github_login_name: String, val id: String, val items_count: Int,
    val linkedin_id: String, val location: String, val name: String, val organization: String, val permanent_id: Int,
    val profile_image_url: String, val team_only: Boolean, val twitter_screen_name: String, val website_url: String
)

QiitaService

Retrofit で Qiita API の GET /api/v2/items を利用できるようにします。今回はページ毎に取得したいので page や per_page のクエリを追加しておきます。Retrofit の実装方法の詳細については公式ドキュメント を閲覧してください。

interface QiitaService {
    @GET("/api/v2/items")
    fun getItems(@Query("page")page: Int, @Query("per_page") perPage: Int): Call<List<Item>>
}

Step2 PagedList でページ毎のデータを管理する

PagedListの生成方法

ページ毎に API からデータを取得する場合は PagedList を生成します。 PagedList の生成は LivePagedListBuilder で行いますが、そのときに DataSource と DataSource.Factory が必要になるので実装します。

class MainViewModel(private val itemDataSourcefactory: ItemDataSourceFactory): ViewModel() {
    val items: LiveData<PagedList<Item>> = LivePagedListBuilder(DataSource.Factory, PagedList.Config).build()
}

ItemDataSource

DataSource で実際にどのようなデータをページ毎に生成するか決めます。今回は RecyclerView が表示されたら 1ページ目のデータ、末尾に到達したら次のページのデータを表示するように実装してみます。(データ取得には先程作成した、Qiitaの記事一覧取得処理を利用します。)

// API呼び出しをしているので、本来であればここで例外の対処を記述する必要がありますが省略しています。
class ItemDataSource(private val service: QiitaService) : PageKeyedDataSource<Int, Item>() {
    // RecyclerView の末尾にデータを追加するときに呼び出される関数
    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) {}

    // RecyclerView のデータを初期化するときに呼び出される関数 
    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Item>) {
        // 1 ページ目のデータを取得する
        val page = 1

        // 1 ページに表示するデータ数
        val perPage = params.requestedLoadSize

        // ページに表示するデータを取得する
        val items = service.getItems(page, perPage).execute().body()

        // 次に表示するページの番号を計算する
        val nextPage = page + 1

        // 取得したデータ、次に表示するページの番号を結果として返す
        callback.onResult(items, null, nextPage)
    }

    // RecyclerView の先頭にデータを追加するときに呼び出される関数
    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) {
        // params.key には 前の loadInitial や loadAfter の呼び出しで返した nextPage が格納されている
        val page = params.key // 1 ページ目のデータを取得する

        // params.requestedLoadSize には 1ページに表示するデータ数が格納されている。
        val perPage = params.requestedLoadSize

        // ページに表示するデータを取得する
        val items = service.getItems(page, perPage).execute().body()

        // 次に表示するページの番号を計算する
        val nextPage = page + 1

        // 取得したデータ、次に表示するページの番号を結果として返す
        callback.onResult(items,   nextPage)
    }
}

ItemDataSourceFactory

DataSource にデータ取得の処理を記述しましたが、DataSource.Factory を通して生成するような仕組みになっています。
実際に LivePagedListBuilder で利用するのは DataSource.Factory になりますので定義してやります。

class ItemDataSourceFactory(service: QiitaService) : DataSource.Factory<Int, Item>() {
    val source = ItemDataSource(service)

    override fun create(): DataSource<Int, Item> {
        return source
    }
}

MainViewModel

ここまで準備できればあとは PagedList を作成するだけです。作成した ItemDataSourceFactory を DI して LivePagedListBuilder に渡します。そして build してやれば PagedList が作成されます。

class MainViewModel(private val itemDataSourcefactory: ItemDataSourceFactory): ViewModel() { 
    private val config = PagedList.Config.Builder().setInitialLoadSizeHint(10).setPageSize(10).build() 
    val items: LiveData<PagedList<Item>> = LivePagedListBuilder(itemDataSourcefactory, config).build() 
    val networkState: LiveData<NetworkState> = itemDataSourcefactory.source.networkState 
} 

Step 3 RecyclerView に PagedList のデータを表示する

先程作成した PagedList を RecyclerView を表示するには PagedListAdapter が必要になります。
次のように PagedListAdapter を継承した Adapter を作成してやります。

ItemRecyclerAdapter

class ItemRecyclerAdapter() : PagedListAdapter<Item, ItemHolder>(diffCallback) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.recycler_view_item, parent, false) as View
        return ItemHolder(view)
    }

    override fun onBindViewHolder(holder: ItemHolder, position: Int) {
        holder.title.text = getItem(position)?.title
    }

    companion object {
        private val diffCallback = object : DiffUtil.ItemCallback<Item>() {
            override fun areItemsTheSame(oldItem: Item, newItem: Item) =
                oldItem.id == newItem.id // check uniqueness

            override fun areContentsTheSame(oldItem: Item, newItem: Item) =
                oldItem == newItem // check contents
        }
    }
}

class ItemHolder(private val view: View) : RecyclerView.ViewHolder(view) {
    val title = view.title
}

recycler_view_item.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="75dp"
    android:layout_margin="8dp"
    app:cardCornerRadius="8dp">

    <TextView
        android:id="@+id/title"
        android:layout_gravity="center|top"
        android:layout_margin="16dp"
        android:textSize="14sp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="TEST" />

</androidx.cardview.widget.CardView>

Step 4 作成したクラスの動作を確認する

あとは今まで作成したクラスを Koin で初期化してセットアップしてやるだけです。
次の定義で QiitaService・ItemDataSourceFactory・MainViewModelを生成できるようにしてやります。

Koin

val appModule = module {
    single {
        Retrofit.Builder()
            .baseUrl("https://qiita.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    single {
        get<Retrofit>().create(QiitaService::class.java)
    }

    single {
        ItemDataSourceFactory(get())
    }

    viewModel {
        MainViewModel(get())
    }
}

そして Koin と RecyclerView のセットアップをしてやればアプリは完成になります。

MainAcitivity

class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModel()
    private val itemRecyclerAdapter = ItemRecyclerAdapter()

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

        // koin 初期化
        startKoin {
            androidLogger()
            androidContext(applicationContext)
            modules(appModule)
        }

        val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = viewModel
        binding.itemRecyclerView.layoutManager = LinearLayoutManager(applicationContext).apply {
            orientation = RecyclerView.VERTICAL
        }
        binding.itemRecyclerView.adapter = itemRecyclerAdapter
        binding.itemRecyclerView.setHasFixedSize(true)
        viewModel.items.observe(this, androidx.lifecycle.Observer {
            itemRecyclerAdapter.submitList(it)
        })
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout>

    <data>

        <variable
            name="viewModel"
            type="jp.kaleidot725.sample.ui.MainViewModel" />
    </data>

    <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=".ui.MainActivity">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/item_recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </FrameLayout>
</layout>

おわりに

ページごとのデータ管理は PageList が中心となって行うようですね。なので PagedList の生成や表示に必要になる周辺クラスを実装していくのが主な作業になりますね。ページごとのデータ管理は難しいと考えていましたが、Androidのライブラリで手厚くサポートされているのでそこまで難しくないですね。

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

Androidで現在地情報を偽装する

はじめに

位置情報を扱うアプリを実装していると現在地情報を特定の場所に変更したいことがあったりします。これまではPlayストアにある現在地偽装アプリをインストールして利用していましたが、異なる開発機で毎回Googleログインをしてインストールするのが手間だったのでアプリ内で実装しました。

注意

今回紹介する現在地情報偽装をした状態でポケGOやDQウォークをするとBAN対象になる可能性があります。当方では責任を負いませんのでご了承ください。

現在地情報偽装機能の実装

権限

位置情報を偽装するためには専用のパーミッションが必要となります。

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> 
    ...
    <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/> 
    ...
</manifest> 

この記述を通常のAndroidManifest.xmlに記載すると以下のエラーとなります。
名称未設定.png

今回はDebug用途での実装となったためこのパーミッションの記述のみ src/debug/AndroidManifest.xml に移しています。debugビルドであればエラーが出たままでもビルドすることはできましたがリリースビルドができるかまでは確認していません。

仮の現在地情報アプリ

設定方法

Android6.0以上で現在地情報を偽装するためには開発者オプションにある 仮の現在地情報アプリを選択 という項目にアプリケーションを指定する必要があります。

通常のアプリはこの項目の対象になりませんが前述の ACCESS_MOCK_LOCATION パーミッションを設定していると仮の現在地情報アプリとして選択できるようになります。

仮の現在地情報アプリとして選択するとこのような表示になります。

設定されているかチェックする

以下のコードで対象のパッケージが仮の現在地情報アプリに設定されているかをチェックすることができます。

private fun isMockAppEnabled(packageName: String): Boolean {
    try {
        val opsManager = getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
        return opsManager.checkOp(AppOpsManager.OPSTR_MOCK_LOCATION, android.os.Process.myUid(), packageName) == AppOpsManager.MODE_ALLOWED
    } catch (e: SecurityException) {
        // MOCK_LOCATIONにアプリが設定されていないとcheckOpでSecurityExceptionが発生する
        return false
    }
}

仮の現在地情報アプリに設定されていない場合は開発者用オプションに遷移して設定してもらいましょう。

if (!isMockAppEnabled(packageName)) {
    showToast("現在地情報を設定するには仮の現在地情報アプリに設定してください")
    startActivity(Intent(android.provider.Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS).apply {
        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    })
    return
}

※開発者オプションを有効化していないとその前の画面までしか遷移できないようです

現在地情報を偽装する

以下のコードでは精度の高いGPSプロバイダ(LocationManager.GPS_PROVIDER)と精度の低いネットワークプロバイダ(LocationManager.NETWORK_PROVIDER)の情報をテスト用プロパイダとして上書きします。
実行後にGoogleMapなどの位置情報アプリを使うと現在地が設定した緯度経度になっていることが確認できます。

val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER)
val locationManager: LocationManager by lazy {
    getSystemService(Context.LOCATION_SERVICE) as LocationManager
}

private fun setMockLocation(latitude: Double, longitude: Double): Boolean {
    try {
        for (provider in providers) {
            locationManager.run {
                addTestProvider(
                    provider,
                    false,
                    false,
                    false,
                    false,
                    false,
                    false,
                    false,
                    Criteria.POWER_LOW,
                    Criteria.ACCURACY_FINE
                )
                setTestProviderEnabled(provider, true)
                setTestProviderLocation(provider, Location(provider).apply {
                    this.latitude = latitude
                    this.longitude = longitude
                    this.altitude = 0.0
                    this.accuracy = 500f
                    this.time = System.currentTimeMillis()
                    this.speed = 0f
                    this.elapsedRealtimeNanos = android.os.SystemClock.elapsedRealtimeNanos()
                })
            }
        }
        return true
    } catch (e: Exception) {
        return false
    }
}

addTestProvider

テスト用プロバイダを作成します。
詳細は割愛しますが様々な設定項目があります。

setTestProviderEnabled

テスト用プロバイダの有効/無効を設定します。

setTestProviderLocation

テスト用プロバイダに対して任意のLocationを設定します。

現在地情報が偽装されたものであるかを確認する

Locationには偽装された位置情報であるかを判断することができるメソッドがあります。
※現在地情報の取得にはACCESS_FINE_LOCATIONパーミッションが許可されている必要があります。

private fun isLocationMocked(): Boolean {
    val locationManager = applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
    val criteria = Criteria().apply { accuracy = Criteria.ACCURACY_FINE }
    val provider = locationManager.getBestProvider(criteria, true)
    val location = locationManager.getLastKnownLocation(provider)
    return location?.isFromMockProvider ?: false
}

現在地情報の偽装を解除する

設定したテスト用プロバイダは以下のコードで解除できます。

private fun clearMockLocation() {
    try {
        for (provider in providers) {
            locationManager.run {
                removeTestProvider(provider)
            }
        }
    } catch (e: IllegalArgumentException) {
        // addTestProviderされていない状態でremoveTestProviderを呼ぶとIllegalArgumentExceptionが発生
    }
}

実用に当たって

紹介した手順にて位置情報の偽装は可能ですがマップのアプリやSDKによっては一度だけの偽装処理は無視される場合があります。恐らくですが位置情報は精度が完全ではないことから正しくない緯度経度を受け取ってしまうことを考慮し関連性のない位置情報を読み捨てするなどの処理が入っていると思われます。
そのため、Serviceなどのバックグラウンドで実行可能なコンポーネントを利用し一定間隔で継続して偽装処理を実行するなどの工夫が必要となりそうです。

位置情報偽装サービスをバックグラウンドで実行するサンプルを実装しました。

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