Android
 Computer >> コンピューター >  >> システム >> Android

Androidのマルチライブラリプロジェクトをマスターする:ローカル・リモート開発のベストプラクティス

はじめに

この記事では、Androidにおけるマルチライブラリプロジェクトについて解説します。特別珍しい話ではありませんが、かといってごく普通のことでもありません。

仕事でマルチライブラリ構成に触れたことがある方もいれば、既存のライブラリをサブモジュールに分割して、より良い構造と整理を目指そうと検討中の方もいるでしょう。どちらの場合でも、飛び込む前に何が待ち受けているのかをしっかり把握しておくことが重要です。

Androidで自作ライブラリを書くのは魅力的な体験です。他の開発者(あるいは将来の自分)の助けになるコードを書くチャンスになります。

ライブラリは単体ではスタンドアロンのプロジェクトとして成立しないため、通常はアプリケーションと同じプロジェクト内でペアとして扱われます。これにより、機能追加やバグ修正を行ったら、そのままプロジェクト内のアプリで即座にテストできるというシンプルな開発サイクルが実現します。つまり、開発者があなたのライブラリを統合する様子をローカルで再現できるわけです。

しかし、もし自分のライブラリが、同じく自分が開発している別のライブラリに依存していたらどうでしょうか?

AARの制約:ローカルライブラリはネストできない

知らない方のために説明すると、ライブラリ(aar)の中に別のローカルライブラリを含めることはできません。リモートの依存関係として参照することはできますが、ローカルのものを組み込むことはできないのです。

これはAndroidではサポートされておらず、これまでいくつかの解決策(FatAarなど)が登場しましたが、必ずしも問題を解決できず、現在ではメンテナンスされていません。Google Issue Trackerにはこの機能を求める要望が長期間オープン状態で続き、コミュニティから大きな注目を集めています。とはいえ、まずはどの壁を壊せて、どの壁が壊せないのかを見極めましょう。

プロジェクトの階層が次のようになっていると想像してください。

-- App
|
 -- OuterLib
 |
 --- InnerLib

InnerLibは元のプロジェクトの一部にできないなら、どこに置けばよいのでしょうか? さらに、InnerLib内部の機能を開発するとき、どうすればローカルで作業できるのでしょうか?

この2つの問いに、本記事で順番に答えていきます。

Gitサブモジュール

ほとんどの技術的問題には、唯一の正解があるわけではありません。通常は複数の解決策が存在し、それぞれに欠点があります。結局のところ、どの欠点なら許容できるかという選択の問題なのです。

最初の問い「InnerLibはどこに置けるのか」に対しては、次の2つの選択肢があります。

  1. InnerLibを元のプロジェクトのサブモジュールにする
  2. InnerLibを独立したリモート依存関係として管理する

Gitのサブモジュールをご存じない方は、公式ドキュメントで概要をつかむのがおすすめです。そこからの引用(冒頭部分)を紹介します。

あるプロジェクトの作業中に、その中で別のプロジェクトを使いたくなることはよくあります。サードパーティが開発したライブラリだったり、自分が別途開発して複数の親プロジェクトで使っているライブラリだったりするかもしれません。こうしたシナリオでは共通の課題が生まれます。2つのプロジェクトを別々のものとして扱いながら、片方をもう片方の中で利用できるようにしたい、ということです。

この記述が示す通り、まさに私たちのユースケースです。サブモジュールを使うメリットは、すべてのコードが1箇所にまとまり、管理が容易で、ローカル開発も簡単なことです。

しかし、サブモジュールには弱点もあります。1つは、サブモジュールがどのブランチを指しているのか常に意識しておく必要がある点です。例えば、メインリポジトリではリリースブランチにいるのに、サブモジュールがフィーチャーブランチを向いていたとしましょう。気づかないままリリースしてしまうと、本番準備が整っていないコードを含んだバージョンを公開することになりかねません。

これを開発者チームの規模で考えてみてください。うっかりミスひとつが高くつく可能性があります。

最初の選択肢に問題を感じるなら、ライブラリを別リポジトリでホストするのが第2の選択肢です。リポジトリのセットアップ自体は簡単ですが、ではローカルではどう作業するのでしょうか?

ローカルでの作業方法

プロジェクトのセットアップが完了すると、OuterLibのbuild.gradleファイルには、おそらく次のような記述があるはずです。

dependencies {
    implementation 'url_to_remote_inner_lib_repository'
}

開発サイクルを効率的で快適なものにするには? InnerLibで機能を開発したとき、OuterLibやアプリケーション上でどうテストすればよいのでしょうか?

思いつく解決策のひとつは、InnerLibをOuterLibのプロジェクトにローカルインポートする方法です。このとき、OuterLib側ではInnerLibを.gitignoreに登録しておきます。Android Studio左側のメニューでプロジェクト名を右クリックし、「New」→「Module」を選択すれば簡単に行えます。開いたウィンドウの左下にある「Import」オプションを選んでください。

ここまでは簡単そうですが、落とし穴はどこにあるのでしょう?

実は、InnerLibに属するファイルを変更しても、その変更は反映されません(.gitignoreで無視されているため)。つまり、変更はすべてInnerLib側で行い、その都度OuterLibに再インポートしなければ結果を確認できないのです。

これはあまり良いやり方とは言えません。もっとスマートな方法が必ずあるはずです。

settings.gradleにほんの数行追加するだけで、InnerLibでの変更が自動的に同期されるようにできます。

InnerLibをプロジェクトにインポートするとき、Android StudioはInnerLibのコピーを作成してキャッシュします。だからこそ、変更のたびに再インポートが必要だったのです。projectDir属性を使えば、Android Studioにファイルの参照先を直接教えることができます。

settings.gradleは通常、次のようになっています。

include ':outerLib', ':innerLib', ':app'

InnerLibをローカル参照するには、settings.gradleを次のように変更します。

include ':outerLib', ':innerLib', ':app'
project(':innerLib').projectDir = new File('PATH_TO_INNER_LIB')

この方法なら、InnerLibのファイルがワーキングディレクトリにリンクされ、変更が即座に反映されます。

しかし、ローカルでOuterLibを開発しつつ、InnerLibはリモート版を使いたい場合の柔軟性も欲しいところです。上記のsettings.gradleの内容はローカル作業専用になってしまうため、このままコミットするのは避けたいですね。

Maven Local

上記の方法にしっくりこない場合は、別のアプローチもあります。ライブラリをMavenで公開するときと同じ要領で、mavenLocalを使ってローカルに公開できるのです。mavenLocalとは、自分のマシン上に置かれたローカルリポジトリ群のことです。

OSごとのmavenLocalのパスは以下の通りです。

  • Mac → /Users/YOUR_USERNAME/.m2
  • Linux → /home/YOUR_USERNAME/.m2
  • Windows → C:\Users\YOUR_USERNAME\.m2

要するに、ライブラリをローカルに公開し、それをプロジェクトから参照できるということです。この方法で、プロジェクトをInnerLibにリンクさせましょう。

この構成を実現するために必要な手順は以下の通りです。

  1. repositories句にmavenLocal()を追加する。これにより、プロジェクトがローカルのリポジトリを検索できるようになります。
buildscript {
    repositories {
        mavenLocal()
    }
}
...
allprojects {
    repositories {
        mavenLocal()
    }
}
  1. dependencies句のimplementation行を、あたかもリモート参照であるかのようにInnerLibを指す形に変更する
  2. InnerLibをローカル公開するため、publishingLocally.gradleというファイルを次の内容で作成する
apply plugin: 'maven-publish'
project.afterEvaluate {
    publishing {
        publications {
            library(MavenPublication) {
                setGroupId groupId // ライブラリのパッケージ
                setArtifactId artifactId
                version versionName // 例: 1.0
                artifact bundleDebugAar
                pom.withXml {
                    def dependenciesNode = asNode().appendNode('dependencies')
                    def dependencyNode = dependenciesNode.appendNode('dependency')
                    dependencyNode.appendNode('groupId', 'your_group_id')
                    dependencyNode.appendNode('artifactId', 'your_artifact_id')
                    dependencyNode.appendNode('version', 'your_version')
                }
            }
        }
    }
}
  1. アプリケーションレベルのbuild.gradleファイルに次の1行を追加する
apply from: './publishingLocally.gradle'

この方法が「良すぎて怪しい」と感じたら――実際その通りです。一方では、まるでリモートライブラリを使っているかのようにシームレスにローカル開発ができます。しかし他方で、ローカル作業中にInnerLibへ変更を加えるたびに、再度ローカル公開が必要になります。大掛かりな作業ではありませんが、単調なタスクを繰り返す負担が生じてしまうのです。

ローカルとリモートを両立させるソリューション

ローカルで変更を加えるたびにInnerLibパッケージを再公開し続けるのは避けたいものです。プロジェクト側に変更を認識させる仕組みが必要です。

前述の「ローカルでの作業方法」セクションでその手法は見つかりましたが、settings.gradleのコミットが課題でした。この問題を解決し、InnerLibをローカル・リモートの両方で扱えるようにするため、gradle.propertiesファイルにパラメータを定義して活用します。

gradle.propertiesは、開発環境を設定するプロジェクトレベルの設定を格納できる場所です。チーム全員の開発環境の一貫性を保つのに役立ちます。

このファイルでお馴染みの設定には、AndroidXサポート(android.useAndroidX=true)やJVM引数(org.gradle.jvmargs=-Xmx1536m)などがあります。

今回の状況を解決するために、ここに「ローカルで作業するかどうか」を示すパラメータを追加しましょう。例えば次のようにです。

workingLocally = false

このパラメータにより、ローカル開発用の設定と本番コード用の設定を切り分けられるようになります。まず、settings.gradleの内容を、パラメータがtrueかどうかをチェックする条件で囲みます。

include ':outerLib', ':innerLib', ':app'
if (workingLocally.toBoolean()) {
    project(':innerLib').projectDir = new File('PATH_TO_INNER_LIB')
}

これで、プロジェクトに対して「InnerLibのファイルをローカルマシンから取得せよ」と指示できます。

もうひとつロジックを変えるべき場所はbuild.gradleです。dependenciesブロックでリモートからライブラリを取得する代わりに、ローカル依存にするかどうかを切り替えられます。

dependencies {
    if (workingLocally.toBoolean()) {
        implementation project(':innerLib')
    } else {
        implementation 'url_to_remote_repository'
    }
}

⚠️ 注意: ローカル作業中は、gradle.propertiesファイルを絶対にコミットしないでください。

道のりは長く、少し疲れる旅だったかもしれません。しかしこれで、マルチライブラリプロジェクトをローカルとリモートの両方で安全かつ効率的に扱える、堅牢なセットアップが手に入りました。

問題に遭遇した場合や、自身の経験・見解を共有したい場合は、ぜひコメントでお知らせください。

  1. AndroidスマホでデスクトップPCを置き換える方法【Maru OSの導入から代替アプリまで】

    現代のAndroidスマートフォンは、私たちが思っている以上に高い性能を秘めています。 メールの送受信、メモの作成、画像編集、ゲーム――スマホでできることは枚挙にいとまがありません。ポケットの中に入っているこの小さな端末は、実質的には一台のパーソナルコンピュータです。世界中のオフィスのデスクに置かれている多くのPCと同等か、それ以上の性能を持っていると言っても過言ではありません。ならば、そのスマホでデスクトップPCを置き換えてみてはどうでしょうか? スマートフォンの「Continuum」と「Convergence」 スマートフォンの性能と柔軟性の向上により、ついにPCを実用的に置き換えられる段

  2. 無料アプリ「KWGT」で実現!Androidホーム画面を自分だけのダッシュボードに変える方法

    クレジット: Oluwademilade Afolabi / MakeUseOf Androidのホーム画面を作り直そうと、最初から考えていたわけではありません。もっと見栄えのいい時計が欲しかっただけなのに、1つのダウンロードが3つになり、気づけばホーム画面のレイアウト全体を見直せるアプリにたどり着いていたのです。 そのアプリがKWGT(Playストアでは「KWGT Kustom Widget Maker」として公開)です。Androidのホーム画面を本格的にパーソナライズできる強力なツールで、本来なら5分で終わるはずの調整が、ダッシュボード全体の再設計へと雪だるま式に発展しました——しか