GlideのDiskLruCacheの実装を読む

この記事では、Glideで使用されている DiskLruCache のキャッシュ情報を保存する処理の実装と仕様についてみていきます。

glide/DiskLruCache.java at v4.11.0 · bumptech/glide

DiskLruCacheの概要

DiskLruCache は、LRUでファイルのキャッシュを行うクラスです。キャッシュ情報の管理には、ジャーナルファイルとメモリ上のデータ構造を使用しています。 ジャーナルファイルについては後で述べますが、キャッシュの読み込みや書き込みを行った履歴を記録したファイルとなっています。 キャッシュの操作を行った時には、はじめにメモリ上の LinkedHashMap に保存された後、ジャーナルファイルへの保存が行われます。
DiskLruCacheインスタンスDiskLruCache.open を使用して取得することができ、 open をした時にジャーナルファイルからメモリへキャッシュ情報が読み込まれます。

ジャーナルファイルの中身

ジャーナルファイルの中身は以下のような形式になっています。

libcore.io.DiskLruCache
1
100
2
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6

はじめの4行はそれぞれ、

libcore.io.DiskLruCache : 固定の文字列
1                       : ディスクキャッシュのバージョン
100                     : アプリが指定するバージョン(Glideでは固定で1)
2                       : ValueCount 1つのkeyに紐づく値の個数(Glideでは固定で1)

を表しています。ディスクキャッシュやアプリが指定するバージョンが変わると、一度キャッシュがすべてクリアされます。

5行目以降がキャッシュ情報の取得や更新のログとなっています。アクションと付加情報がスペース区切りで1行ごとに記されていて、アクションは表に示す4種類があります。

アクション 付加情報1 付加情報2~
READ キャッシュキー -
REMOVE キャッシュキー -
DIRTY キャッシュキー -
CLEAN キャッシュキー キャッシュしている各ファイルのサイズ ValueCountの数

各アクションの意味

それぞれのアクションについて説明していきたいと思います。
READ/REMOVE は想像がつきやすいですが、キャッシュ情報の取得/削除を行った時のアクションです。
個人的には、データベースなどに関する知識があまりないため、DIRTYCLEAN が表す意味を理解するのが少し難しかったです。
DiskLruCache では、キャッシュ情報を書き換える際に DiskLruCache#edit などを使用して取得できる Editor クラスを介する必要があります。アプリ側では、取得した Editor に対してキャッシュ情報の追加や上書きを行った後に Editor#commit を呼び出すことで、実際に画像ファイルキャッシュの更新が行われます。
この時に Editor を取得してから commit を行うまでの状態が DIRTY となります。( DIRTY のログが書き込まれるのはEditor 作成時)
DIRTY となっている情報を commit し終わると CLEAN のログが書き込まれます。

キャッシュのmax sizeを超えた時の処理

何度も画像のキャッシュを行っていくと、当然ですがキャッシュサイズが増えていきmax sizeを越えることがあります。そのサイズを超えた時は、以下の処理が行われてキャッシュサイズがmax size以下に抑えられます。

  1. メモリキャッシュの古い情報から順番に消していき合計サイズがmax size以下になるようにする
  2. 無駄なログが一定量ある場合はメモリキャッシュを元にジャーナルファイルを整理する(必要なログだけ残す)

ここでいう 無駄なログ には何パターンかありますが、例えば

  • キーA の情報が 編集A(DIRTY) -> 更新A(CLEAN) -> 取得(READ) -> .. -> 更新X(CLEAN) と操作された時の最後の DIRTYまたはCLEAN 以外のログ
    • キーA の最終的な情報がキャッシュに保存されていれば良いので、それまでの更新履歴などは不要
  • キャッシュ情報を取得した時のログ(READ)

その他の処理

キャッシュ情報の追加や削除を行う時には上記のメモリキャッシュの更新とジャーナルファイルへのログの記録が主な処理となっています。
しかし、エラーが起きる可能性もあり、その場合は基本的にキャッシュ情報の削除が行われます(キャッシュは消しても問題はないはずなので安全な方にたおしている)

また、 DIRTY から CLEAN へ状態がうつる場合の処理についても少し触れておきます。
DIRTYCLEAN はそれぞれ別々のファイルとして画像を保存するようになっていて、commit 時に DIRTY 用のファイルから CLEAN 用のファイルへリネームが行われます。 glide/DiskLruCache.java at v4.11.0 · bumptech/glide

キャッシュファイルの名前の規則

DIRTY 用ファイルの名前: ${key}.${index}.tmp
CLEAN 用ファイルの名前: ${key}.${index}

glide/DiskLruCache.java at v4.11.0 · bumptech/glide

まとめ

DiskLruCache ではただ画像ファイルを保存しているだけではなく、キャッシュの操作ログも記録をしておくことでLRUとして動作することを可能にしていた。

ジャーナルファイルについてはデータベースの処理とも関連がありそうなので今後さらに調べていきたいです。

Androidのxmlで画像がうまく読み込めずにハマった時のメモ

xmlbitmap タグを使って画像を読み込む際に以下のようなエラーが起きた場合と、9-patch画像がただの画像になってハマった時のメモ

Caused by: org.xmlpull.v1.XmlPullParserException: Binary XML file line #7: <bitmap> requires a valid 'src' attribute
    at android.graphics.drawable.BitmapDrawable.updateStateFromTypedArray(BitmapDrawable.java:851)

エラーが出た時の状況

my_drawable.xml ファイルのルートに以下のような画像を設定していました。

<?xml version="1.0" encoding="utf-8"?>
<bitmap
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@drawable/ic_launcher_round"
    />

ic_launcher_round は以下の階層にあるpng画像とベクター画像でした。

mipmap/
    ic_launcher_round.png
mipmap-anydpi-v26/
    ic_launcher_round.xml

エラーの原因と解決方法

bitmap タグで設定した画像は BitmapDrawable クラスとして表現され、jpegpngなどの画像しか対応されておらず、今回の場合にAPI26以上ではベクター画像が使われるため、無効な画像として扱われていました。

解決方法の1つとして、API26以上の場合にもpng画像を使用する方法があります。他にも画像を使う側でAPIレベルによって処理を分けるという方法もあります。(使う画像を変えて対応するか、使う側の処理を分けて対応するか)

9-patch画像がただの画像として表示される問題

こちらも先ほどと原因は同じで、9-patch画像は NinePatchDrawable に変換される必要があり、 BitmapDrawable ではただのpng画像と同じ扱いになります。
解決方法として、 bitmap ではなく nine-patch タグを使用する方法があります。

参考

bitmap タグなど、xmlをパースする時の処理 DrawableInflater.java - Source

Drawable リソースのドキュメント ドローアブル リソース  |  Android デベロッパー  |  Android Developers

Drawable のリファレンス Drawable  |  Android デベロッパー  |  Android Developers

Glideのディスクキャッシュ処理のメモ

ディスクキャッシュのインスタンスの生成

ディスクキャッシュの処理を行うインスタンスは、 Glide.Builder で設定した DiskCache.Factory から作成されます。もしBuilderで設定しない場合は InternalCacheDiskCacheFactory が使われます。

if (diskCacheFactory == null) {
    diskCacheFactory = new InternalCacheDiskCacheFactory(context);
}

https://github.com/bumptech/glide/blob/v4.11.0/library/src/main/java/com/bumptech/glide/GlideBuilder.java#L540

InternalCacheDiskCacheFactory

InternalCacheDiskCacheFactoryDiskLruCacheFactory を継承しているクラスとなっていて、 名前からLRUキャッシュが関係していそうなことが分かります。

InternalCacheDiskCacheFactory のコンストラクタは以下のようになっています。
キャッシュサイズは DiskCache.Factory.DEFAULT_DISK_CACHE_SIZE = 250MB 、保存場所はアプリのキャッシュディレクトリ内が指定されているようです。

public InternalCacheDiskCacheFactory(Context context) {
    this(
        context,
        DiskCache.Factory.DEFAULT_DISK_CACHE_DIR,
        DiskCache.Factory.DEFAULT_DISK_CACHE_SIZE);
}

public InternalCacheDiskCacheFactory(
    final Context context,
    final String diskCacheName,
    long diskCacheSize
) {
    super(
        new CacheDirectoryGetter() {
            @Override
            public File getCacheDirectory() {
                File cacheDirectory = context.getCacheDir();
                if (cacheDirectory == null) {
                    return null;
                }
                if (diskCacheName != null) {
                    return new File(cacheDirectory, diskCacheName);
                }
                return cacheDirectory;
            }
        },
        diskCacheSize);
}

DiskLruCacheFactoryが生成するクラス

InternalCacheDiskCacheFactory が継承している DiskLruCacheFactory とファクトリが生成するクラスについてみていきます。
DiskLruCacheFactoryDiskCache のクラスを生成するための DiskCache.Factory インタフェースを実装しています。
DiskCache を生成する処理はいかのようになっており、 DiskLruCacheWrapper を生成していることが分かりました。

public DiskCache build() {
    ...
    return DiskLruCacheWrapper.create(cacheDir, diskCacheSize);
}

DiskCacheインタフェースについて

ここまでで、 DiskCache の実装クラスとして DiskLruCacheWrapper が使用されることが分かりました。
そもそも DiskCache はどのようなインターフェースなのかをみておきます。

DiskCache は以下のように、キャッシュキーを元にファイルを取得したり保存したりするためのメソッドが4つだけあります。 DiskCache.Writer は指定されたファイルへデータを書き込むためのインタフェースになっています。

void clear()
void delete(Key key)
File get(Key key)
void put(Key key, DiskCache.Writer writer)

DiskCache (glide API)

DiskLruCacheWrapperの処理

DiskCache インタフェースの役割を確認できたので、実際にどのような処理が行われているかをみていきたいと思いますが、 DiskLruCacheWrapper はクラス名の通り DiskLruCache をラップしたクラスとなっていて、キャッシュに関する主要な処理のほとんどは DiskLruCache で行われています。 DiskLruCache の生成処理は以下のようになっています。

private synchronized DiskLruCache getDiskCache() throws IOException {
    if (diskLruCache == null) {
        diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
    }
    return diskLruCache;
}

DiskLruCacheWrapper では DiskLruCache を扱うために、キャッシュキー(Key型)を String へ変換する処理も行っています。
キーの変換処理は SafeKeyGeneratorcalculateHexStringDigest で行われ、 Key#updateDiskCacheKey から取得できるバイト配列をSHA256でハッシュ化しているようです。

return Util.sha256BytesToHex(container.messageDigest.digest());

まとめ

Glideのデフォルトのディスクキャッシュは、LRUキャッシュで250MBまでキャッシュされることが分かりました。
(ちなみにドキュメントに書いてあります Glide v4 : Configuration - Disk Cache)

途中で出てきたキャッシュキー Key に使われる値については Glide v4 : Caching に記述があります。

In Glide 4, all cache keys contain at least two elements:

  1. The model the load is requested for (File, Uri, Url). If you are using a custom model, it needs to > correctly implements hashCode() and equals()
  2. An optional Signature

In fact, the cache keys for steps 1-3 (Active resources, memory cache, resource disk cache) also > include a number of other pieces of data including:

  1. The width and height
  2. The optional Transformation
  3. Any added Options
  4. The requested data type (Bitmap, GIF, etc)

デバッグの際には Engine タグで出力されるログが参考になります

adb shell setprop log.tag.Engine VERBOSE
V/Engine: Started new load in 0.594687ms, key: EngineKey{
    model=content://media/external/images/media/322064,
    width=1050,
    height=1560,
    resourceClass=class java.lang.Object, 
    transcodeClass=class android.graphics.drawable.Drawable, 
    signature=com.bumptech.glide.signature.MediaStoreSignature@9ccb4e57,
    hashCode=-1605280832,
    transformations={class android.graphics.drawable.Drawable=com.bumptech.glide.load.resource.bitmap.DrawableTransformation@5db7ce1d, class android.graphics.Bitmap=com.bumptech.glide.load.resource.bitmap.FitCenter@5db7ce1d, class com.bumptech.glide.load.resource.gif.GifDrawable=com.bumptech.glide.load.resource.gif.GifDrawableTransformation@5db7ce1d, class android.graphics.drawable.BitmapDrawable=com.bumptech.glide.load.resource.bitmap.DrawableTransformation@5db7ce1d},
    options=Options{values={Option{key='com.bumptech.glide.load.resource.bitmap.Downsampler.DownsampleStrategy'}=com.bumptech.glide.load.resource.bitmap.DownsampleStrategy$FitCenter@2fc7c43}}}

Glide.withに渡す引数による処理の違いについて

Androidの画像読み込みライブラリの Glide で画像を読み込む際に、 Glide.with に渡す引数による処理の違いについて書きます。

Glide.withの処理を追いかける

Glide.with では以下のように getRetriever(Context) を使って取得した RequestManagerRetrieverget メソッドに with の仮引数を渡して、最終的に RequestManager を取得しています。 この流れは仮引数の型が ActivityView の場合も同様です。

public static RequestManager with(@NonNull Context context) {
    return getRetriever(context).get(context);
}
private static RequestManagerRetriever getRetriever(@Nullable Context context) {
    ...
    return Glide.get(context).getRequestManagerRetriever();
}

RequestManager を生成するまで

RequestManagerRetriever#get には Glide.with と同じく ContextActivity, View など様々な引数をとるメソッドが定義されています。

まずはじめに、 ApplicationContext を指定した場合の処理を見ていきます。 get(Context) のコードは以下のようになっており、 FragmentActivityActivity などにキャストできる場合はキャストしてから再度 get を呼び出しています。

ApplicationContext の場合は getApplicationManager(Context) が呼ばれ、その中で RequestManager が作成されます。

@NonNull
public RequestManager get(@NonNull Context context) {
  if (context == null) {
      ...
  } else if (Util.isOnMainThread() && !(context instanceof Application)) {
    if (context instanceof FragmentActivity) {
      return get((FragmentActivity) context);
    } else if (context instanceof Activity) {
      return get((Activity) context);
    } else if (context instanceof ContextWrapper
        && ((ContextWrapper) context).getBaseContext().getApplicationContext() != null) {
      return get(((ContextWrapper) context).getBaseContext());
    }
  }
  return getApplicationManager(context);
}
private RequestManager getApplicationManager(@NonNull Context context) {
    ...
    if (applicationManager == null) {
        Glide glide = Glide.get(context.getApplicationContext());
        applicationManager =
            factory.build(
                glide,
                new ApplicationLifecycle(),
                new EmptyRequestManagerTreeNode(),
                context.getApplicationContext());
    }
    ...
    return applicationManager;
}

次に Activity を指定した場合や、ContextActivity にキャストできた場合の処理について見ていきます。 バックグラウンドスレッドで呼び出された場合は、先ほどみた ApplicationContext を指定した時と同じ処理が呼ばれます。
そうでない場合は Factory から RequestManager を生成します。生成したインスタンスRequestManager を管理するための Fragment で保持されています。
getRequestManagerFragment では、 1つの FragmentManager に対して1つの RequestManagerFragment が保持されるように処理が行われています。

public RequestManager get(@NonNull Activity activity) {
    if (Util.isOnBackgroundThread()) {
        return get(activity.getApplicationContext());
    } else {
        assertNotDestroyed(activity);
        android.app.FragmentManager fm = activity.getFragmentManager();
        return fragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
    }
}
private RequestManager fragmentGet(
        @NonNull Context context,
        @NonNull android.app.FragmentManager fm,
        @Nullable android.app.Fragment parentHint,
        boolean isParentVisible) {
    RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible);
    RequestManager requestManager = current.getRequestManager();
    if (requestManager == null) {
        Glide glide = Glide.get(context);
        requestManager =
            factory.build(glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
        current.setRequestManager(requestManager);
    }
    return requestManager;
}

FragmentFragmentActivity, androidxの Fragment の場合も FragmentManager を取得して Activity の時と同様の処理が行われます。
また、 View の場合は View#getContext を元に Activity とその配下のすべての Fragment を取得して、 View が属する FragmentActivity を探し出しています。

RequestManagerRetriever#get(View)
RequestManagerRetriever#findSupportFragment

RequestManagerを生成した後

ここまでで Glide.with の返り値である RequestManager を生成するタイミングが特定でき、以下のように大きく2パターンの RequestManager が生成されることがわかりました。

ApplicationContextまたはバックグラウンドスレッド上で生成

factory.build(
    glide,
    new ApplicationLifecycle(),
    new EmptyRequestManagerTreeNode(),
    context.getApplicationContext()
);

それ以外

factory.build(
    glide,
    current.getGlideLifecycle(), 
    current.getRequestManagerTreeNode(), 
    context
);

factory

Factoryは、GlideBuilder で指定することができます。指定しない場合は RequestManagerRetriever.DEFAULT_FACTORY が使用されます。
RequestManagerRetriever.DEFAULT_FACTORY

Lifecycleによる違い

RequestManager を生成する処理で、注目したいことは Lifecycle の違いです。
ApplicationLifecycle は常にアクティブなライフサイクルとなっています。
一方、 RequestManagerFragment#getGlideLifecycle()Fragment のライフサイクルと等しくなります。

Glideでは、この Lifecycle に応じて画像読み込みリクエストの開始/中断などが行われます。例えば、 Fragment で画像を読み込みリクエストを開始し、読み込み完了前に Fragment が破棄された時にリクエストを中断するということが可能です。
バックグラウンドスレッドでリクエストを行う時には、 ActivityFragment のライフサイクルに応じてリクエストを止めたくないケースであることが想定されるため、 ApplicationLifecycle で実行させているということが分かりました。

まとめ

Glide.with に渡す引数によって、画像読み込み処理のライフサイクルが変わります。
ActivityFragment と同じライフサイクルで画像を読み込めば十分な時には、 ActivityFragment, View を渡すと端末のリソースが無駄に使用されることがなくなります。そうでない場合は、ApplicationContext を渡したり、バックグラウンドスレッド上で Glide.with を呼び出す必要があります。

Jetpack Benchmarkを使ってView#measureにかかる時間を計測する

この記事は、 Android Advent Calendar 2019 - Qiita の23日目です。

はじめに

この記事では、ベンチマークを行うためのJetpack Benchmarkライブラリの紹介と、ベンチマークの例として View#measure にかかる時間の計測を行います。

Jetpack Benchmarkとは

ベンチマークを行うためのライブラリで、Instrumented Tests( androidTest ディレクトリ内のテスト)を書くかのようにベンチマークを行うことができます。

Benchmark app code  |  Android Developers

Jetpack Benchmarkの設定

ベンチマーク用のモジュールを追加

ベンチマーク用のコードを追加するためのモジュールを作成します。
Jetpack Benchmarkを使う際には debuggable=false を指定する必要があるため、モジュールを分けておくと継続的に実行する際に便利です。

プラグインとライブラリを追加

ルートの build.gradleclasspathを設定した後、モジュールの build.gradleプラグインとライブラリを追加します。

classpath "androidx.benchmark:benchmark-gradle-plugin:1.0.0"
apply plugin: 'androidx.benchmark'
androidTestImplementation "androidx.benchmark:benchmark-junit4:1.0.0"

また、モジュール作成時に記述されている defaultConfig 内の testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" を削除しておきます。

debuggableをfalseに設定する

AndroidTest を実行する場合のみ debuggable=false にするためには module/src/androidTest/AndroidManifest.xml を作成後、以下のコードを追加します。

<application
    android:debuggable="false"
    tools:ignore="HardcodedDebugMode"
    tools:replace="android:debuggable"/>

以上で設定は終わりとなります。ここからは実際にベンチマークを取るためのコードを書いていきます。

サンプルのベンチマークを実行

androidTest/java/package ディレクトリに以下のクラスを作成します。

@RunWith(AndroidJUnit4::class)
class ViewMeasureTest {
    @get:Rule
    val benchmarkRule = BenchmarkRule()

    @Test
    fun benchmarkSomeWork() = benchmarkRule.measureRepeated {
        Thread.sleep(100)
    }
}

注目部分は以下の2点です。

  • BenchmarkRule の設定
  • テストコードで benchmarkRule.measureRepeated を使用

measureRepeated 内に実行する処理が、ベンチマークの対象となります。
ベンチマークテストは通常のテストと同様に実行するだけで、ベンチマークをとることができます。今回は結果が分かりやすいように100 msのスリープ処理を実行するようにしました。
実行してみると以下のような出力が行われます。

benchmark:   100,160,468 ns ViewMeasureTest.benchmarkSomeWork

処理に 100,160,468 ns ≒ 100 ms かかったという出力が得られました。

このようにJetpack Benchmarkでは、テストコードと同じ記述方法でベンチマークをとることができます。

View#measureにかかる時間を計測する

もう1つの例として、weight を使った LinearLayout入れ子にした場合としなかった場合の View#measure の計測を行います。 (weight入れ子で使用するとAndroidStudioでは Nested weights are bad for performance というinspectionが表示されます。)

レイアウトファイル

使用したレイアウトファイルは以下の2つです

レイアウトプレビューは以下の画像のようになります。 f:id:scache:20191223004901p:plain

ベンチマーク用コード

以下のコードを実行して計測を行いました。
ベンチマークテストでは measureRepeated 内で実行する処理の計測を行いますが、 runWithTimingDisabled 内に書いた処理は計測から除外することが可能です。

@Test
fun noNestedWeights() {
    benchmarkRule.measureRepeated {
        val container: ViewGroup = runWithTimingDisabled {
            val context = ApplicationProvider.getApplicationContext<Context>()
            LayoutInflater.from(context).inflate(R.layout.layout_no_nested, null) as ViewGroup
        }
        measureAndLayoutWrapLength(container)
    }
}

private fun measureAndLayoutWrapLength(container: ViewGroup) {
    val widthMeasureSpec = MeasureSpec.makeMeasureSpec(360, MeasureSpec.EXACTLY)
    val heightMeasureSpec = MeasureSpec.makeMeasureSpec(640, MeasureSpec.AT_MOST)
    container.measure(widthMeasureSpec, heightMeasureSpec)
    container.layout(
        0, 0, container.measuredWidth,
        container.measuredHeight
    )
}

計測結果

ベンチマークテストを実行した結果は以下のようになりました。 レイアウト構造が少し異なるため一概には言えませんが、 weight入れ子で使用した場合は、しなかった場合と比べて処理に大きく時間がかかっていることが分かります。

入れ子にした場合 : benchmark:       475,000 ns ViewMeasureTest.nestedWeights
入れ子にしない場合: benchmark:       233,802 ns ViewMeasureTest.noNestedWeights

最後に

Jetpack Benchmarkライブラリを使用することで、簡単に安定したベンチマークをとることができます。
今回は View#measure を例にしましたが、公式のサンプルにあるオートボクシングのようにAndroidに依存していないコードのベンチマーク計測も可能です。

公式のサンプルはこちら performance-samples/BenchmarkSample at master · android/performance-samples · GitHub

例で使用したコードなどは以下のレポジトリで確認することができます。 github.com

Scratchファイルを使って気軽に実行可能なコードを保存する

概要

この記事ではIntellijでサポートされたKotlinのScratchファイルについて紹介を行います
Scratchファイル自体はKotlinに限らずJavaなどもサポートされています

Scratchファイルとは

Kotlinなどの実行可能なコードを保存するファイルで、デフォルトではプロジェクト外に保存されます。 プロジェクト内にあるファイルと違う点は、あらかじめプロジェクトをビルドしておけば、Scratchファイルのビルドだけで実行可能なため、実行までの時間が速いことです。

IntelliJIDE機能の一つです

実行環境

  • IntelliJ IDEA 2018.3.1 (Community Edition)
  • Kotlin Plugin 1.3.10-release-IJ2018.3-1

Hellow World!

  1. プロジェクトにファイルを追加する時と同じように New -> Scratch File を選択 f:id:scache:20181214142223p:plain:w300

  2. Scratchファイルの形式が選べるので Kotlin を選択
    f:id:scache:20181214142253p:plain:w300

  3. ファイルが作成されるので Use classpath of Module からモジュールを選択してコードを記述します
    f:id:scache:20181214143310p:plain:w600

実行してみる

左上にある実行ボタンを押すとコードが実行され、エディタ上に結果が表示されます
※実行前にはモジュールのBuildをするか Make Before Run にチェックする必要があります
各行ごとに出力が表示されるのがとても便利です
f:id:scache:20181214143243p:plain:w600

選択したモジュール内のクラスを使う

Scratchファイルでは、モジュールの中で定義されているクラスを使用することが可能です
例えば以下のようなクラスが定義されている場合、Scratchファイルでも使用することが可能です

package com.github.sckm

data class Person(
    val name: String,
    val age: Int
)

f:id:scache:20181214163219p:plain:w600

ライブラリを使用する

ライブラリを使用する際は、Scratchファイル用に依存を追加することはできず、
使用するモジュールの build.gradle などで依存を追加する必要があります

使用例

自分がよく使うパターンとしては、

  • プロジェクトでたまに使うAPIのレスポンスを確認したい時
    • IDE上でできるので1番よく使用しています
  • ちょっとした処理の動作を確認したい時
    • テストコードとして実行すると時間がかかるプロジェクトで時間を節約可能

Scratchファイルの保存場所

~/Library/Preferences/IdeaIC2018.3/scratches/

プロジェクトとは関係無い場所に保存がされるため間違えてGitリポジトリに含めてしまうという心配もありません

もちろん、IDE上からでもファイルを開くこともできます f:id:scache:20181214153353p:plain:w400

まとめ

  • コード実行時に新しくプロジェクトを作ったり、Git配下にファイルを追加する必要が無い
  • モジュール内のクラスを使用可能
  • テストコードと似た感覚でカジュアルに実行可能

参考

Scratches - Help | IntelliJ IDEA
Tuning IntelliJ IDEA - Help | IntelliJ IDEA

ブログに実行可能なKotlinコードを貼り付ける

ブログなどのサイト上で実行可能なKotlinコードを表示するためのスクリプト(Kotlin Playground)がJetBrainsから公開され、 Try Kotlin や Kotlinのリファレンスにあるような機能を自分のサイトでも使うことが可能になりました。

f:id:scache:20180428035614p:plain:w300

all - Kotlin Programming Language

使い方

scriptの記述

codeタグ内にコードを記述する場合は以下のコードをヘッダーなどに記述します。

<script src="https://unpkg.com/kotlin-playground@1" data-selector="code"></script>

上の記述ではcodeタグすべてにKotlinPlaygroundが適用されてしまいます。
はてなブログMarkdownで使う場合には以下のようにKotlinコードのみで適用されるようにするのが良いでしょう。
以後の説明ではこちらを使っていると仮定して進めていきます。

<script src="https://unpkg.com/kotlin-playground@1"></script>
<script>
  document.addEventListener('DOMContentLoaded', function() {
    KotlinPlayground('.lang-kotlin');
  });
</script>

以上で準備は終了になります。

コードの記述

はてなブログMarkdownの場合は、以下のようにコードを記述します f:id:scache:20180428035057p:plain:w300

実際の表示

以下にKotlin Playgroundを使ったコードを表示しています。

fun main(args : Array<String>) {
    println("Hello Kotliner!")
    println("Click this green button at the top right!")
}

最後に

  • はてなブログなど、自分で自由にいじるのが難しい環境では少し扱いずらい部分がある
  • KotlinPlaygroundが少し重い?

さらに詳しい情報は以下を参照してください

Embedding Kotlin Playground | Kotlin Blog

github.com