20201028のAndroidに関する記事は9件です。

#24 Kotlin Koans Collections/Max min 解説

1 はじめに

Kotlin公式リファレンスのKotlin Koans Collections/Max minの解説記事です。

Kotlin Koansを通してKotlinを学習される人の参考になれば幸いです。

ただし、リファレンスを自力で読む力を養いたい方は、
すぐにこの記事に目を通さないで下さい!

一度各自で挑戦してから、お目通し頂ければと思います:fist:

2 max()/min()/maxBy()/minBy()

max():コレクションの要素の最大値を返す。

min():コレクションの要素の最小値を返す。

maxBy():コレクションが渡す引数(selector)として渡す条件式の戻り値のうち、最大のものを返す。

minBy():コレクションが渡す引数(selector)として渡す条件式の戻り値のうち、最小のものを返す。

(いずれの関数もKotlin1.4からDeprecatedになっています(2020年8月17日より)。それぞれ代わりとなる関数が用意されているので一度調べて見てください。)

3 Collections/Max minの解説

Kotlin Koans Collections/Max minの解説です。
随時本サイトの内容を引用させていただきます。

本文とコードを見てみましょう。

Implement Shop.getCustomerWithMaximumNumberOfOrders() and Customer.getMostExpensiveOrderedProduct() using max, min, maxBy, or minBy.

listOf(1, 42, 4).max() == 42
listOf("a", "ab").minBy { it.length } == "a"
Max_min
// Return a customer whose order count is the highest among all customers
fun Shop.getCustomerWithMaximumNumberOfOrders(): Customer? = TODO()

// Return the most expensive product which has been ordered
fun Customer.getMostExpensiveOrderedProduct(): Product? = TODO()

Shop.getCustomerWithMaximumNumberOfOrders(): Customer?から考えて見ましょう。

各クラスのプロパティは以下のようになっています。

Shopクラス:name(String型)、customers(List< Customer >型)

Customerクラス:name(String型)、city(City型)、orders(List< Order >型)

Orderクラス:products(List< Product >型)、isDelivered(Boolean型)

Shopのプロパティcustomersのorders(Orderを要素としてもつ)のうち要素数が最大のものを返すように実装します。

要素数を表すのには、コレクションクラスのプロパティsizeを使用します。

したがって、以下のような実装になります。

Max_min
fun Shop.getCustomerWithMaximumNumberOfOrders(): Customer? = customers.maxBy{it.orders.size}  


次に、Customer.getMostExpensiveOrderedProduct(): Product?について考えましょう。

各クラスのプロパティは以下のようになっています。

Customerクラス:name(String型)、city(City型)、orders(List< Order >型)

Orderクラス:products(List< Product >型)、isDelivered(Boolean型)

Productクラス:name(String型)、price(Double型)

Customerのプロパティorders(OrderインスタンスのList)のproducts(ProductインスタンスのList)のうち最大のpriceを持つものを返すように実装します。

まず、flatMap()を利用してCustomerの購入したproductsを抽出します。

Max_min
fun Customer.getMostExpensiveOrderedProduct(): Product? = orders.flatMap{it.products}

そのなかで、maxBy()を利用して最大のpriceをもつものを返すように実装します。

したがって、最終的なコードは以下のようになります。

Max_min
fun Customer.getMostExpensiveOrderedProduct(): Product? = orders.flatMap{it.products}.maxBy{it.price}

4 最後に

次回はKotlin Koans Collections/Sortの解説をします:muscle:

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

#23 Kotlin Koans Collections/FlatMap 解説

1 はじめに

Kotlin公式リファレンスのKotlin Koans Collections/FlatMapの解説記事です。

Kotlin Koansを通してKotlinを学習される人の参考になれば幸いです。

ただし、リファレンスを自力で読む力を養いたい方は、
すぐにこの記事に目を通さないで下さい!

一度各自で挑戦してから、お目通し頂ければと思います:fist:

2 flatMap()

flatMap():呼び出し元のコレクションの要素を連結してListとして返す。

val result = listOf("abc", "12").flatMap { it.toCharList() }
result == listOf('a', 'b', 'c', '1', '2')

上の例では、abc12という要素を持ったListがflatMap()にラムダ式を渡し、戻り値を連結してListを生成しています。

3 Collections/FlatMapの解説

Kotlin Koans Collections/FlatMapの解説です。
随時本サイトの内容を引用させていただきます。

本文とコードを見てみましょう。

Implement Customer.getOrderedProducts() and Shop.getAllOrderedProducts() using flatMap.

val result = listOf("abc", "12").flatMap { it.toCharList() }
result == listOf('a', 'b', 'c', '1', '2')
FlatMap
// Return all products this customer has ordered
val Customer.orderedProducts: Set<Product> get() {
    TODO()
}

// Return all products that were ordered by at least one customer
val Shop.allOrderedProducts: Set<Product> get() {
    TODO()
}

初めに、Customer.orderedProductsを参照したときのTODO()の実装を考えましょう。

各クラスのプロパティは以下のようなっています。

Customerクラス:name(String型)、city(City型)、orders(List< Order >型)

Orderクラス:products(List< Product >型)、isDelivered(Boolean型)

Customer.orderedProductsを参照したとき、Customerインスタンスが持つordersプロパティの各要素(Order)のproductsのSetを返すように実装します。

つまり、Customer→orders(OrderのList)→products(ProductのList)の順にプロパティを参照しています。

したがって、以下のようなコードになります。

FlatMap
val Customer.orderedProducts: Set<Product> get() {
    return orders.flatMap{it.products}.toSet()
}

次に、Shop.allOrderedProductsを参照したときのTODO()の実装を考えましょう。

各クラスのプロパティは以下のようになっています。

Shopクラス:name(String型)、customers(List< Customer >型)

Customerクラス:name(String型)、city(City型)、orders(List< Order >型)

Orderクラス:products(List< Product >型)、isDelivered(Boolean型)

Shop.allOrderedProductsを参照したとき、Shop型インスタンスのcustomersの各要素(Customer)のordersの各要素(Order)のproductsのSetを返すように実装します。

つまり、Shop→customers(CustomerのList)→orders(OrderのList)→products(ProductのList)の順にプロパティを参照しています。

太線部の参照関係はCustomer.orderedProductsと同様なので、これを利用します。

したがって、以下のような実装になります。

FlatMap
 return customers.flatMap{it.orderedProducts}.toSet()

4 最後に

次回はKotlin Koans Collections/Max minの解説をします:muscle:

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

UnityでAndroidのプラグインのクラスが見つからなくて(ClassNotFoundException)何故か使えないときに怪しむべきProGuard、Minify

Androidのプラグイン何故か使えない

俺は完璧に設定しているのにビルドして実行するとClassNotFoundExceptionでAndroidプラグインが使えないことがあって時間を費やしたのですが、いろいろ調べた結果ProGuardのMinifyという処理が原因でした。ClassNotFoundExceptionの原因調べててProGuardまでたどり着かなくね?という懸念があったので共有します。
自分がたどり着いたのはこれにProGuradの記述があったからです。。。

リリースビルドはMinifyが有効になっている

デフォルトの状態でリリースビルドするとProGuardによるMinify処理が行われるようになっているみたいです。
チェックが入っていてMinifyを切るとプラグインの機能が使えるようになりました。
スクリーンショット 2020-10-28 16.45.17.png

ProGuardって何?

ProGuardはアプリのアプリの圧縮、難読化、最適化の機能みたいですね。
アプリの圧縮、難読化、最適化
この処理によってプラグインで使っている機能まで取り去られるみたいなことが起きているみたいです。

ProGuardの設定

ProGuardの設定ファイルを作成することで勝手に処理されない部分を指定できるようになっているようです。Minifyを切ってしまうとこの機能の恩恵が受けられなくなるのでこの方法で対処するのがベターなのではないでしょうか。

スクリーンショット 2020-10-28 16.46.14.png

この Custom Proguard File というチェックをつけるとproguard-user.txtが作成されるのでそれを編集することで設定できます。

proguard-user.txt
-keep class android.support.v4.content.** {
    *;
}

例えばandroid.support.v4.contentでClassNotFoundExceptionが出ていた場合はこんなふうに指定する感じです。この状態でビルドするとMinify対象外になる感じですかね。別の書き方もあるようですので、リンクを参考してください。

まとめ

  • 何故かClassNotFoundExceptionが出たときはMinifyを疑ってみよう。
  • ProGuardの設定で対処しよう。
  • これわかりづらいから何とかならんのか。。。

参考

Unity に Admob 実装した時、Proguard や Minify を無効にせずに Releaseビルドする方法
Unity Android ビルドの Minify オプションの罠

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

【Android】switch/caseでR.id.XXXXを書いたら警告が出た

事の発端

アクションバーの戻るボタン実装方法を調べていて、
以下の書き方の記事がよく見かけたので、

@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
    int id = item.getItemId();
    switch (id) {
        case R.id.home:
            // ...
            break;
        default:
            break;
    }
    return super.onOptionsItemSelected(item);
}

その通りに実装したら、こういう警告がでてしまいました。

Resource IDs will be non-final in Android Gradle Plugin version 5.0, avoid using them in switch case statement.

if-else文のほうが良いらしい

AndroidStudioのガイドでは、
R.id.XXは、ビルドするまでfinal staticではないから、らしい。

なので、if-else文で判定するようにしたら警告は消えました。

@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
    int id = item.getItemId();
    if (id == R.id.home) {
      // ...
    } else {

    }
    return super.onOptionsItemSelected(item);
}

どうしてもswitch/caseを書きたい場合

gradle(モジュール)のLintを無効にすれば良いようです。

android {

    lintOptions {
        disable 'NonConstantResourceId'
    }
}

環境

  • Android Studio
    • 4.1
  • Android Gradleプラグインバージョン
    • 4.1.0
  • Gradleバージョン
    • 6.5
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Flutter】デバイスから画像や動画を取得(Image Picker)

概要

パブリックへのアップロードを実装する際に、ローカルデバイスのギャラリーから画像や動画を取得し一旦保持する処理が必要になります。それを今回はプラグインとして提供されているImage Pickerで実装していきます。

手順

0.iOS用のセットアップ
info.plist
<key>NSPhotoLibraryUsageDescription</key>
<string>This app requires to access your photo library</string>
<key>NSCameraUsageDescription</key>
<string>This app requires to add file to your camera</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app requires to add file to your photo library your microphone</string>
1.パッケージの導入

画像も動画もimage_pickerを使います。
以下のように、dependencies:の配下に記載しましょう。

pubspec.yaml
dependencies:
  image_picker: ^0.6.7+11
2.インポート
import 'dart:io';
import 'package:image_picker/image_picker.dart';
3.ファイルを取得

Stateクラスの中に定義します。

class _UploadScreenState extends State<UploadScreen> {
  File _video;
  final picker = ImagePicker();

  Future getVideoFromGalley() async {
//  画像の場合
//  final pickedFile = await picker.getImage(source: ImageSource.gallery);
    final pickedFile = await picker.getVideo(source: ImageSource.gallery);
    setState(() {
      if (pickedFile != null) {
        _video = File(pickedFile.path);
      } else {
        print('No video selected');
      }
    });
  }

上記はギャラリーから取得する場合。
カメラから取得したい場合は、source: ImageSource.cameraとしてください。
これで_videoに画像や動画の中身が格納されたのであとは煮るなり焼くなりしてください。

Tips

アップロード用ファイルをサイズで制限したい時、ファイルオブジェクトからサイズを取得できます。(バイト単位)

  _videoSize = _video.lengthSync();

メガバイト単位で少数第3位まで取得したい場合

  _videoSize = ((((_video.lengthSync()/1048576)*1000).round())/1000);

さいごに

お疲れ様でした!

参考サイト

https://pub.dev/packages/image_picker

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

【Flutter】超便利な動画再生ウィジェットChewie

概要

Flutterで動画プレイヤーを実装したい時、ググってまず出てくるのがvideo_playerプラグインだと思います。(https://pub.dev/packages/video_player)
こちらでも実装は可能なのですが、再生や停止、インジゲーターの表示などもControllerを用いて自身で管理しなければいけないので少々面倒です。そこで、これらを自動的に構成してくれるchewieウィジェットというものをご紹介したいと思います。ちなみにvideo_playerとは完全に別物ではなく、chewieもvideo_playerをベースとしています。 (https://pub.dev/packages/chewie)

ちなみになぜchewie(チューインガム)というのかはわかりません(笑)

前提条件

[✓] Flutter (Channel stable, 1.22.2, on Mac OS X 10.15.7 19H2, locale ja-JP)

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
[✓] Xcode - develop for iOS and macOS (Xcode 12.0.1)
[✓] Android Studio (version 4.0)
[✓] Connected device (1 available)

• No issues found!

手順

0.完成形

シンプルに画面中央にビデオプレイヤーを配置する画面を作っていきます。
動画のソースはgithubに公開されているものを使用しています。
Screenshot_1603861186.png

1.パッケージの導入

pubspec.yamlにchewieとvideo_playerのパッケージを追加します。
記事作成時(2020年10月27日)時点のchewieの最新版は0.9.10ですが、依存関係となっているvideo_playerは0.10.0までしか対応していませんので一旦は以下のバージョンにしましょう。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  chewie: ^0.9.10
  video_player: ^0.10.0

記述したら、Pub getを実行。

2.パッケージのインポート

以下の三つをインポートしてください。

main.dart
import 'package:chewie/chewie.dart';
import 'package:chewie/src/chewie_player.dart';
import 'package:video_player/video_player.dart';

3.コントローラーもろもろ

概要でも記載した通り、ChewieControllerはVideoPlayerControllerと依存関係にあるので、Stateクラス内にVideoPlayerControllerとChewieControllerの両方を初期化します。

main.dart
class _MyHomePageState extends State<MyHomePage> {
//コントローラーの定義
  VideoPlayerController _videoPlayerController;
  ChewieController _chewieController;

//初期化
  @override
  void initState() {
    super.initState();
    _videoPlayerController = VideoPlayerController.network(
        'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4');
    _chewieController = ChewieController(
      videoPlayerController: _videoPlayerController,
      aspectRatio: 3 / 2,  //アスペクト比
      autoPlay: false,  //自動再生
      looping: true,  //繰り返し再生

      // 以下はオプション(なくてもOK)
      showControls: false,  //コントロールバーの表示(デフォルトではtrue)
      materialProgressColors: ChewieProgressColors(
        playedColor: Colors.red,  //再生済み部分(左側)の色
        handleColor: Colors.blue,  //再生地点を示すハンドルの色
        backgroundColor: Colors.grey,  //再生前のプログレスバーの色
        bufferedColor: Colors.lightGreen,  //未再生部分(右側)の色
      ),
      placeholder: Container(
        color: Colors.grey,  //動画読み込み前の背景色
      ),
      autoInitialize: true,  //widget呼び出し時に動画を読み込むかどうか
    );
  }
//コントローラーの破棄
  @override
  void dispose() {
    _videoPlayerController.dispose();
    _chewieController.dispose();
    super.dispose();
  }
//・・・

4.画面表示

超シンプルですね!

main.dart
child: Chewie(
   controller: _chewieController,
),

5.ソースコード全体

全体のコードは以下のようになります。

main.dart
import 'package:flutter/material.dart';
import 'package:chewie/chewie.dart';
import 'package:chewie/src/chewie_player.dart';
import 'package:video_player/video_player.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Chewie Sample',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Chewie Sample'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  VideoPlayerController _videoPlayerController;
  ChewieController _chewieController;

  @override
  void initState() {
    super.initState();
    _videoPlayerController = VideoPlayerController.network(
        'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4');
    _chewieController = ChewieController(
      videoPlayerController: _videoPlayerController,
      aspectRatio: 3 / 2,
      autoPlay: false,
      looping: true,
      // Try playing around with some of these other options:

      //showControls: false,
      materialProgressColors: ChewieProgressColors(
        playedColor: Colors.red,
        handleColor: Colors.blue,
        backgroundColor: Colors.grey,
        bufferedColor: Colors.lightGreen,
      ),
      placeholder: Container(
        color: Colors.grey,
      ),
      autoInitialize: true,
    );
  }
  @override
  void dispose() {
    _videoPlayerController.dispose();
    _chewieController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Expanded(
              child: Center(
                child: Chewie(
                  controller: _chewieController,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

注意

現状、iOSエミュレータでは正常に動作しないようです。実際に試したところ、Controllerを定義しているのにもかかわらず、
controller != null, 'You must provide a chewie controller'
みたいなエラーが表示されます。Androidエミュレータや実機(iOS含め)では正常に動作しますのでそちらで試してみてください。

さいごに

video_playerのみだと、ステートの管理が面倒なところをchewieがいい感じに全部やってくれます。いろいろ自分でカスタマイズしたいときはvideo_playerが良いかと思いますが、とりあえず動画が見れればいい!という場合はchewieで十分だと思います。
Flutterはまだまだ日本語の情報が少ないため、今後も役に立つ情報を発信できたらと思います!

参考サイト

https://pub.dev/packages/video_player
https://pub.dev/packages/chewie

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

Android ViewPager ページ指定

// adapterには、表示用の配列またはArrayListが設定されていること
viewPager.setAdapter(adapter);

// positionにページのindex値を指定
// 遅延処理をしないと、index値が0~4の場合、表示されない
viewPager.postDelayed(() -> viewPager.setCurrentItem(position), 50);

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

Android Fragment remove replace できない

レイアウトxmlに定義してあるフラグメントはremove、replaceできない

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

Android Jetpack をフル活用して、Qiita の LGTM 数ゼロの記事だけ検索できるアプリを作った

Android Jetpack をフル活用して、Qiita の LGTM 数ゼロの記事だけを検索できるアプリを作りました。

Qiita0LgtmViewer.gif

作ったきっかけ

日本テレビの「月曜から夜ふかし」が好きでよく見るのですが、その中の「再生回数 100 以下の動画を調査」を見て、めちゃくちゃ面白いなと思いました。
自分は Qiita によく記事をアップしているのですが、LGTM がゼロのまま埋れてしまうことがあり、もったいないなと思っていました。
再生回数 100 以下の動画を見て、埋れているから価値が無いかというとそういうわけでは無いんだなと気づき、Qiita でも似たようなことしたら面白そうだなと思ったのがきっかけです。
また、ついでに Android Jetpack についてキャッチアップしたかったというのもあります。

Android Jetpack とは

以下、公式サイトの引用です。

Jetpack はライブラリ スイートです。デベロッパーは Jetpack を使用することで、おすすめの方法に沿って、ボイラープレート コードを削減し、Android の複数のバージョンとデバイスにわたって一貫して機能するコードを作成できるので、コードの重要な部分に集中できます。

使用したライブラリ・アーキテクチャ

アーキテクチャはアプリ アーキテクチャ ガイドにのっとって MVVM パターンを採用しました。

解説

Android Jetpack が提供する機能に絞って解説します。

データ バインディング ライブラリ

データ バインディング ライブラリとは、プログラムではなく宣言形式を使用して、レイアウト内の UI コンポーネントをアプリのデータソースにバインドできるサポートライブラリです。
まず、以下のようなデータクラスを用意しました。

Article.kt
/**
 * 記事データ
 * JSONのキー名に合わせている
 * @param id 記事の一意なID
 * @param title 記事のタイトル
 * @param likes_count この記事への「LGTM!」の数(Qiitaでのみ有効)
 * @param url 記事のURL
 * @param user Qiita上のユーザを表します。
 */
@Parcelize
data class Article(
    val id: String,
    val title: String,
    val likes_count: Int,
    val url: String,
    val user: User
) : Parcelable

拡張性を考慮して Parcelable 化していますが、今回必須ではありません。
プロパティ名をスネークケースにしたのは、Retrofit で受け取った JSON 形式のレスポンスをパースするためです。

Extensions.kt
/**
 * ImageViewにloadImageメソッドを追加するための拡張関数
 * @param url 画像URL
 */
@BindingAdapter("imageUrl")
fun ImageView.loadImage(url: String) {
    Glide.with(context).load(url).into(this)
}

Glide を使って ImageView に画像を読み込ませるために拡張関数を用意しました。
また、@BindingAdapter("imageUrl")を付与することで、view_article.xmlで用意した拡張関数を呼べるようにしました。
view_article.xmlは以下の通りです。

view_article.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:bind="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="article"
            type="com.kmdhtsh.qiita0lgtmviewer.entity.Article" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="@dimen/view_article_padding">

        <ImageView
            android:id="@+id/profile_image_view"
            android:layout_width="@dimen/profile_image_view_width"
            android:layout_height="@dimen/profile_image_view_height"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            bind:imageUrl="@{article.user.profile_image_url}"
            tools:background="#f00" />

        <TextView
            android:id="@+id/title_text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/title_text_view_margin_start"
            android:ellipsize="end"
            android:maxLines="2"
            android:text="@{article.title}"
            android:textSize="@dimen/title_text_view_text_size"
            app:layout_constraintStart_toEndOf="@id/profile_image_view"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="記事のタイトル記事のタイトル記事のタイトル記事のタイトル記事のタイトル記事のタイトル記事のタイトル" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/user_name_text_view_margin_top"
            android:ellipsize="end"
            android:singleLine="true"
            android:text="@{article.user.name}"
            android:textSize="@dimen/user_name_text_view_text_size"
            app:layout_constraintStart_toStartOf="@id/title_text_view"
            app:layout_constraintTop_toBottomOf="@id/title_text_view"
            tools:text="ユーザの名前ユーザの名前ユーザの名前ユーザの名前ユーザの名前" />

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

LiveData

LiveData とは監視可能なデータホルダークラスです。通常の監視とは異なり、LiveData はActivityFragmentなどのライフサイクルに考慮した監視が可能です。
以下のようなArticleListViewModelクラスを用意しました。

ArticleListViewModel.kt
/**
 * 記事表示用ViewModel
 */
class ArticleListViewModel @ViewModelInject constructor(
    private val searchRepository: SearchRepository
) :
    ViewModel() {
    // 記事一覧(読み書き用)
    // MutableLiveDataだと受け取った側でも値を操作できてしまうので、読み取り用のLiveDataも用意しておく
    private val _articleList = MutableLiveData<Result<List<Article>>>()
    val articleList: LiveData<Result<List<Article>>> = _articleList

    /**
     * 検索処理
     * @param page ページ番号 (1から100まで)
     * @param perPage 1ページあたりに含まれる要素数 (1から100まで)
     * @param query 検索クエリ
     */
    fun search(page: Int, perPage: Int, query: String) = viewModelScope.launch {
        try {
            Timber.d("search start")
            val response = searchRepository.search(page.toString(), perPage.toString(), query)

            // Responseに失敗しても何かしら返す
            val result = if (response.isSuccessful) {
                response.body()!!
            } else {
                mutableListOf()
            }

            // LGTM数0の記事だけに絞る
            val filteredResult = result.filter {
                it.likes_count == 0
            }
            // viewModelScopeはメインスレッドなので、setValueで値をセットする
            _articleList.value = Result.success(filteredResult)
            Timber.d("search finish")
        } catch (e: Throwable) {
            _articleList.value = Result.failure(e)
        }
    }
}

まず、Repository から取得した値を加工し、_articleList.valueで値を更新します(ちなみにメインスレッド以外で更新する場合はpostValueを使うようにしてください)。
すると、ArticleListFragmentのライフサイクルの状態を見計ってviewModel.articleList.observeにデータが流れます。

ArticleListFragment.kt
viewModel.articleList.observe(viewLifecycleOwner, { result ->
    result.fold(
        {
            articleList.addAll(it)
            articleRecyclerViewAdapter.notifyDataSetChanged()
        },
        {
            Timber.e(it)
        }
    )
})

Hilt

Hilt とは Dagger をベースとして作られた DI ライブラリです。
Dagger よりも簡単に DI を実現できます。
また、Dagger と Hilt は、同じコードベース内で共存できます。

まず、Application クラスを継承したクラスを作成し、@HiltAndroidAppアノテーションを付与します。

MainApplication.kt
// Applicationクラスには@HiltAndroidAppが必要
@HiltAndroidApp
class MainApplication : Application() {
    ...
}

次に、ActivityFragmentなど依存関係を注入したいクラスに@AndroidEntryPointアノテーションを付与します。

MainActivity.kt
// DI対象のFragmentの下にあるActivityにも@AndroidEntryPointが必要
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    ...
}

なお、Fragmentに依存関係を注入する場合、Fragmentの下にあるActivityにも@AndroidEntryPointアノテーションが必要です。

ArticleListFragment.kt
/**
 * 記事一覧表示用Fragment
 * DI対象のFragmentには@AndroidEntryPointを付ける必要がある
 */
@AndroidEntryPoint
class ArticleListFragment : Fragment() {
    ...
}

次に Repository クラスのコンストラクタの引数に@Injectアノテーションを使用して、そのクラスのインスタンス提供方法を Hilt に知らせます。

SearchRepository.kt
/**
 * 検索用Repository
 */
class SearchRepository @Inject constructor(private val searchService: SearchService) {
    ...
}

次に ViewModel クラスのコンストラクタの引数に@ViewModelInjectアノテーションを使用して、そのクラスのインスタンス提供方法を Hilt に知らせます。

ArticleListViewModel.kt
/**
 * 記事表示用ViewModel
 */
class ArticleListViewModel @ViewModelInject constructor(
    private val searchRepository: SearchRepository
) : ViewModel() {
    ...
}

@ViewModelInjectアノテーションは ViewModel クラス限定のアノテーションで、これによって以下のように ViewModel クラスのインスタンスを生成できるようになります。

ArticleListFragment.kt
private val viewModel: ArticleListViewModel by viewModels()

最後に、@Injectアノテーションを付けた箇所などにインスタンスの実体モジュールを提供するための Hilt モジュールを作成します。

ApplicationProvidesModule.kt
/**
 * DI用ProvidesModule
 * @Inject が付いたプロパティや引数に提供する値の実体を定義
 */
@Module
@InstallIn(ApplicationComponent::class)
object ApplicationProvidesModule {

    /**
     * HttpLoggingInterceptorの提供
     */
    @Provides
    fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor {
        // OkHttp側でもTimberを使用する
        val logging = HttpLoggingInterceptor {
            Timber.tag("OkHttp").d(it)
        }
        logging.setLevel(HttpLoggingInterceptor.Level.BASIC)
        return logging
    }

    /**
     * OkHttpClientの提供
     * @param httpLoggingInterceptor
     */
    @Provides
    fun provideOkHttpClient(
        httpLoggingInterceptor: HttpLoggingInterceptor
    ): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(httpLoggingInterceptor)
            .build()
    }

    /**
     * SearchServiceの提供
     * @param okHttpClient
     */
    @Provides
    fun provideSearchService(okHttpClient: OkHttpClient): SearchService {
        return Retrofit.Builder()
            .baseUrl("https://qiita.com")
            .client(okHttpClient)
            .addConverterFactory(MoshiConverterFactory.create())
            .build()
            .create(SearchService::class.java)
    }

    /**
     * SearchRepositoryの提供
     * @param searchService
     */
    @Provides
    fun provideSearchRepository(searchService: SearchService): SearchRepository {
        return SearchRepository(searchService)
    }
}

@InstallIn(ApplicationComponent::class)アノテーションは、Applicationクラスをインジェクション対象とするという意味です。これにより、すべてのActivityFragmentで使えるようになります。
@Providesアノテーションが提供するインスタンスの実体です。
なお、どうやら@Providesアノテーションを付与した関数のインスタンスに関しては、@Injectアノテーションを付けなくても提供されるようです。

まとめ

Android Jetpack をフル活用して、Qiita の LGTM 数ゼロの記事だけを検索できるアプリを作りました。
Android Jetpack のおかげで、比較的簡単に保守性の高い MVVM パターンのコードを書くことができました。
またアプリ自体はシンプルなので、ライブラリの導入も容易にでき、実際に手を動かすことで各種ライブラリの理解を深めることができました。
実際に、Android Jetpack は便利な機能がたくさんあるので、Android エンジニアの方は絶対覚えた方が良いです。
この経験を実務にも生かしていきたいです。

今後の課題

あくまでライブラリの勉強に重きをおいたので、UI は最小限しか作っていません。
改善点として、読み込み時のプログレスバーの表示・非表示だったり、リストの並び替え機能などがあります。
また、テストコードや CI/CD の環境も整えるとさらに良いかなと思っています。
改善に関しては、もし反響があればやってみようかなと思っています。

ソースコード

hiesiea/Qiita0LgtmViewer

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