マルチモジュール構成に便利な「Dependency Graph」を書き出して視覚化する

まずは、開発環境にいれておきます。

グラフの書き出しに必要です。


$ brew install graphviz

Graphviz - Graph Visualization Software



つづいて、確認するマルチモジュールプロジェクト の build.gradle に追記。


apply from: file('gradle/dependencyGraph.gradle')

該当位置に、gradleタスクを記述したファイルを置きます。

学習コストがかかるので、安定のJakeコードをそのままコピペで

まずは稼動させて、モチベーションを上げて進みましょう。


# gradle/dependencyGraph.gradle

task projectDependencyGraph {
  doLast {
    def dot = new File(rootProject.buildDir, 'reports/dependency-graph/project.dot')
    dot.parentFile.mkdirs()
    dot.delete()

    dot << 'digraph {\n'
    dot << "  graph [label=\"${rootProject.name}\n \",labelloc=t,fontsize=30,ranksep=1.4];\n"
    dot << '  node [style=filled, fillcolor="#bbbbbb"];\n'
    dot << '  rankdir=TB;\n'

    def rootProjects = []
    def queue = [rootProject]
    while (!queue.isEmpty()) {
      def project = queue.remove(0)
      rootProjects.add(project)
      queue.addAll(project.childProjects.values())
    }

    def projects = new LinkedHashSet<Project>()
    def dependencies = new LinkedHashMap<Tuple2<Project, Project>, List<String>>()
    def multiplatformProjects = []
    def jsProjects = []
    def androidProjects = []
    def javaProjects = []

    queue = [rootProject]
    while (!queue.isEmpty()) {
      def project = queue.remove(0)
      queue.addAll(project.childProjects.values())

      if (project.plugins.hasPlugin('org.jetbrains.kotlin.multiplatform')) {
        multiplatformProjects.add(project)
      }
      if (project.plugins.hasPlugin('kotlin2js')) {
        jsProjects.add(project)
      }
      if (project.plugins.hasPlugin('com.android.library') || project.plugins.hasPlugin('com.android.application')) {
        androidProjects.add(project)
      }
      if (project.plugins.hasPlugin('java-library') || project.plugins.hasPlugin('java')) {
        javaProjects.add(project)
      }

      project.configurations.all { config ->
        config.dependencies
                .withType(ProjectDependency)
                .collect { it.dependencyProject }
                .each { dependency ->
          projects.add(project)
          projects.add(dependency)
          rootProjects.remove(dependency)

          def graphKey = new Tuple2<Project, Project>(project, dependency)
          def traits = dependencies.computeIfAbsent(graphKey) { new ArrayList<String>() }

          if (config.name.toLowerCase().endsWith('implementation')) {
            traits.add('style=dotted')
          }
        }
      }
    }

    projects = projects.sort { it.path }

    dot << '\n  # Projects\n\n'
    for (project in projects) {
      def traits = []

      if (rootProjects.contains(project)) {
        traits.add('shape=box')
      }

      if (multiplatformProjects.contains(project)) {
        traits.add('fillcolor="#ffd2b3"')
      } else if (jsProjects.contains(project)) {
        traits.add('fillcolor="#ffffba"')
      } else if (androidProjects.contains(project)) {
        traits.add('fillcolor="#baffc9"')
      } else if (javaProjects.contains(project)) {
        traits.add('fillcolor="#ffb3ba"')
      } else {
        traits.add('fillcolor="#eeeeee"')
      }

      dot << "  \"${project.path}\" [${traits.join(", ")}];\n"
    }

    dot << '\n  {rank = same;'
    for (project in projects) {
      if (rootProjects.contains(project)) {
        dot << " \"${project.path}\";"
      }
    }
    dot << '}\n'

    dot << '\n  # Dependencies\n\n'
    dependencies.forEach { key, traits ->
      dot << "  \"${key.first.path}\" -> \"${key.second.path}\""
      if (!traits.isEmpty()) {
        dot << " [${traits.join(", ")}]"
      }
      dot << '\n'
    }

    dot << '}\n'

    def p = 'dot -Tpng -O project.dot'.execute([], dot.parentFile)
    p.waitFor()
    if (p.exitValue() != 0) {
      throw new RuntimeException(p.errorStream.text)
    }

    println("Project module dependency graph created at ${dot.absolutePath}.png")
  }
}

projectDependencyGraph.gradle · JakeWharton/SdkSearch

以上の2ファイルに対する作業を、

シンプルなマルチモジュール構成のコードをお借りしてやってみます。

あとは、タスク実行するだけ。

以下動画のようにPNG画像としてグラフが書き出されます。



DoroidKaigi2019 コードでも利用されていますね。

conference-app-2019/dependencyGraph.gradle at master · DroidKaigi/conference-app-2019

便利です。


SpaceX REST API で試す Retrofit の coroutine 対応

Retrofit の coroutine 対応が進んでいるようです。

Jake Wharton's retrofit2-kotlin-coroutines-adapter has been the go-to solution for bridging the coroutine world with Retrofit for a little while. But now, a much-anticipated PR has finally been merged, officially bringing coroutine support to Retrofit 2.

Retrofit meets coroutines - zsmb.co

JakeWharton/retrofit2-kotlin-coroutines-adapter: A Retrofit 2 adapter for Kotlin coroutine's Deferred type.

SpaceX-API とか公開されていたのですね。

r-spacex/SpaceX-API: Open Source REST API for rocket, core, capsule, pad, and launch data

https://api.spacexdata.com/v3/rockets

対応する最小限のデータクラスを作って、試してみましょう。


data class Rocket(

    val id: Int,

    @field:Json(name = "rocket_name") 
    val name: String

)


val retrofit = Retrofit.Builder()
    .baseUrl("https://api.spacexdata.com/v3/")
    .addConverterFactory(MoshiConverterFactory.create())
    .build()

val api = retrofit.create<SpaceXApi>()

create() は reified type が使えるようになってます。

JSONのシリアライズはお好みのものを。

そして、interface を書いていきますが、

ここを戻り値に別に見てみます。

Call<List<Rocket>>

interface はこれまでと同じ記述。


interface SpaceXApi {

  @GET("rockets")
  fun getRockets(): Call<List<Rocket>>

}

受け側の3つの例。


runBlocking {
  val rockets: List<Rocket> = api.getRockets().await()
  rockets.forEach(::println)
}


runBlocking {
  val rockets: List<Rocket> = try {
    api.getRockets().await()
  } catch (e: Exception) {
    println("Network error :[")
    return@runBlocking
  }
  rockets.forEach(::println)
}


runBlocking {
  val response = api.getRockets().awaitResponse()
  if (response.code() == 200) {
    response.body()?.forEach(::println)
  }
}

結果はすべて同じ。


Rocket(id=1, name=Falcon 1)
Rocket(id=2, name=Falcon 9)
Rocket(id=3, name=Falcon Heavy)
Rocket(id=4, name=Big Falcon Rocket)

これまで非同期処理時に必要だった コールバック や enqueue の記述は必要ありません。

受け側の記述を変化させることで、Exception や レスポンスコードを拾うことができます。

suspend List<Rocket>

suspend を使うことでさらに直感的に書けます。


interface SpaceXApi {

  @GET("rockets")
  suspend fun getRockets(): List<Rocket>

}


runBlocking {
  val rockets = api.getRockets()
  rockets.forEach(::println)
}

結果。


Rocket(id=1, name=Falcon 1)
Rocket(id=2, name=Falcon 9)
Rocket(id=3, name=Falcon Heavy)
Rocket(id=4, name=Big Falcon Rocket)

List<Rocket> に対して Call や Deferred のようなラッパーは必要ありません。

suspend Response<List<Rocket>>

Response を拾います。


interface SpaceXApi {

  @GET("rockets")
  suspend fun getRockets(): Response<List<Rocket>>

}


runBlocking {
  val response = api.getRockets()
  if (response.code() == 200) {
    response.body()?.forEach(::println)
  }
}

結果。


Rocket(id=1, name=Falcon 1)
Rocket(id=2, name=Falcon 9)
Rocket(id=3, name=Falcon Heavy)
Rocket(id=4, name=Big Falcon Rocket)

最も実用的で簡潔な記述と思われます。

まとめ

Kotlin で記述するにしても、多くの記述が可能となりそうです。

RxJava他ライブラリを利用しての非同期実装を含めると様々となります。

ネット上で検索するにも自分の環境や好みに合った記述を探すのにも時間がかかることになり混乱もありそうで。

Jakeも今後はメンテしないと思われます。

Márton Braun on Twitter: "Retrofit is finally receiving the proper coroutine support it deserves! Read more about it in my latest blog post here: https://t.co/8k9EauzzUa #AndroidDev #Kotlin #Coroutines" / Twitter

ちなみに、これらの話は今現在はSNAPSHOTで進行中です。

以下でどうぞ。


repositories {
  maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
}

dependencies {
  implementation "com.squareup.retrofit2:retrofit:2.5.1-SNAPSHOT"
}


いまさらAndroid端末にすばやくWiFiで接続する

なんとなく面倒なのでスクリプトで。

USBで接続のあと #5555 で待たせて再起動する。

「restart」なので、kill-server → start-server は不要。


adb tcpip 5555

IPアドレスをコマンドラインから取得。


ip="$(adb shell ip route | awk '{print $9}')"

そのIPにWiFi接続。


adb connect $ip



 

まとめ

ついでに、開発時には邪魔なスリープもOFFに。


adb shell svc power stayon true

How to prevent an android device from entering sleep (via adb command shell) - Stack Overflow

connect_via_wifi.sh


#!/bin/sh

adb tcpip 5555

sleep 1

ip="$(adb shell ip route | awk '{print $9}')"

adb connect $ip

adb shell svc power stayon true

adb devices -l

端末内 adbd の TCPモード(#5555)が起動している限り再接続が可能。

なので、USB接続なしの状態でも

IPの目処つけて 「adb connect」 でどうぞ。


~ $ adb devices
List of devices attached

~ $ get-oui -v
Renaming ieee-oui.txt to ieee-oui.txt.bak
Fetching OUI data from http://standards-oui.ieee.org/oui/oui.txt
Fetched 4084734 bytes
Opening output file ieee-oui.txt
25968 OUI entries written to file ieee-oui.txt

~ $ sudo arp-scan -l
Interface: en0, datalink type: EN10MB (Ethernet)
Starting arp-scan 1.9.5 with 256 hosts (https://github.com/royhills/arp-scan)
192.168.0.1	8a:af:ec:9c:91:08	BUFFALO.INC
192.168.0.2	dc:23:76:3d:9b:75	HTC Corporation
192.168.0.3	d0:27:82:f5:e8:2a	AzureWave Technology Inc.
192.168.0.8	8c:85:00:14:89:d1	Murata Manufacturing Co., Ltd.
192.168.0.4	a2:37:e3:12:9a:88	HTC Corporation
192.168.0.6	33:28:6d:29:7b:72	Google, Inc.

~ $ adb connect 192.168.0.4
connected to 192.168.0.4:5555

~ $ adb devices
List of devices attached
192.168.0.4:5555	device

初回の、USBケーブルなしでTCPモードを起動できないのはセキュリティ絡みでか。

ADBオーバーネットワークでWiFiで複数端末を一括接続する

Android Devices Being Shipped with TCP Port 5555 Enabled - DEV Community 👩‍💻👨‍💻