Upload
letram
View
219
Download
0
Embed Size (px)
Citation preview
Me• SVP of Polyglot Engineering @ Cogent
• Rails, iOS, etc.
• Co-Founder & CTO Oomph
• 4 shipping + 1 prototype Android app
• > 100 bespoke iOS apps
• ~250 iOS apps on Oomph platform
• Enterprise Java, HPC, “big data” background
• Founder of BFPG
What?
• Experiences building a native Android app
• Oomph Viewer, Subaru Symmetry, etc.
• Not looking at Phonegap, Ximian, Titanium, etc.
• Not looking at “non-Scala” problems
• Not teaching you Scala
• Lots of code, the deck will be available
App Goals
• Oomph - digital publishing, think mags on iPads
• A technical proof of concept client for Android
• Build as a platform not a single app
• Develop in parallel with server
• No Java
• Take advantage of Scala language & library support
Why Scala?• Had done Java before, don’t want to use again
• “Better Java”
• Succinct/less boilerplate - closures, type classes, type aliases, type
inference, no semi-colons, no ‘.’
• Features - immutability, equational reasoning, functions, case
classes, implicits, packages, mixins, currying/partial application, etc.
• Standard & other library support - option, either, future, etc.
• Cool stuff! scalaz, actors, higher-kinds, etc.
• Share concepts/code/DB schema with server
• Monadic code FTW!
Example 1 - Java
context.runOnUiThread(new Runnable() { @Override public void run() { dialog.dismiss(); String standaloneStartUrl = "file://" + context.getFilesDir() + "/content/index.html"; Intent next = new Intent(context, IssueViewerActivity.class); next.putExtra("start_url", standaloneStartUrl); context.startActivity(next); context.finish(); } });
Example 1 - Scala
context.runOnUiThread { dialog.dismiss() val standaloneStartUrl = "file://" + context.getFilesDir + "/content/index.html" val next = new Intent(context, classOf[IssueViewerActivity]) next.putExtra("start_url", standaloneStartUrl) context.startActivity(next) context.finish() }
Example 2 - Java
Button button = new Button(context); button.setText("Greet"); button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText( context, "Hello!", Toast.LENGTH_SHORT).show(); } }); layout.addView(button);
Example 2 - Scala
val button = new Button(context) button.setText("Greet") button.setOnClickListener(new OnClickListener() { def onClick(v: View) { Toast.makeText( context, "Hello!", Toast.LENGTH_SHORT).show() } }) layout.addView(button)
Example 2 - Scala
val button = new Button(context) button.setText("Greet") button.setOnClickListener(Toast.makeText( context, "Hello!", Toast.LENGTH_SHORT).show()) layout.addView(button)
Objections
• “Functions slow down VM”
• “Too many small classes”
• Not a problem in practice (YMMV)
• Can proguard away
• Language issues…
Toolchain
• sbt
• Android Plugin [1]
• Scalastyle - stylechecker
• IntelliJ w/ Scala plugin
• TeamCity
[1] https://github.com/pfn/android-sdk-plugin
sbT
• “Simple” build tool
• Native Scala build tool
• Not great, but better than alternatives
• Supported by TypeSafe
sbt Plugin
• Most mature at the time
• Requires giter8
• All the things you’d need: emulator & device
support, signing, tests, etc.
IntelliJ
• Awesome IDE
• Multiple platforms - RubyMine, AppCode, PHP Storm,
etc.
• Familiar
• Decent support for Scala & sbt
Other choices
• Eclipse (?)
• Android Studio (IntelliJ)
• Android SDK Plugin [1]
• Scaloid template [2] (uses [1])
[1] https://github.com/pfn/android-sdk-plugin [2] https://github.com/pocorall/hello-scaloid-sbt
Scaloid
• Take advantage of language features
• Simplifies common patterns
• Alerts, inter-activity comms
• DSL for UI building
Scaloidvar connectivityListener: BroadcastReceiver = null !def onResume() { super.onResume() connectivityListener = new BroadcastReceiver { def onReceive(context: Context, intent: Intent) { doSomething() } } registerReceiver(connectivityListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)) } !def onPause() { unregisterReceiver(connectivityListener) super.onPause() }
Scaloid
broadcastReceiver(ConnectivityManager.CONNECTIVITY_ACTION) { (context, intent) => doSomething() }
Scaloid
new AsyncTask[String, Void, String] { def doInBackground(params: Array[String]) = { doAJobTakeSomeTime(params) } ! override def onPostExecute(result: String) { alert("Done!", result) } }.execute("param")
scalaz
• An extension to the core Scala library for
functional programming
• New datatypes (Validation, NonEmptyList, etc.)
• Extensions to standard classes (OptionOps,
ListOps, etc.)
• Implementations of general functions you need
(ad-hoc polymorphism, traits + implicits)
Argonautval input = """ [ { "name": "Mark", "age": 191 }, { "name": "Fred", "age": 33, "greeting": "hey ho, lets go!" }, { "name": "Barney", "age": 35, "address": { "street": "rock street", "number": 10, "post_code": 2039 }} ] """ val people = input.decodeOption[List[Person]].getOrElse(Nil) val nice = people.map(person => person.copy(greeting = person.greeting.orElse(Some("Hello good sir!")))) val result = nice.asJson println(result.spaces4) assert(result.array.exists(_.length == 3))
Specs2
final class HelloWorldSpec extends Specification { "The 'Hello world' string" should { "contain 11 characters" in { "Hello world" must have size(11) } "start with 'Hello'" in { "Hello world" must startWith("Hello") } "end with 'world'" in { "Hello world" must endWith("world") } } }
Slick
• Type safe DB access library for Scala
• Supported by TypeSafe
• Works well on device or server
• Had used in several projects before (ScalaQuery)
• Added in integration with Android DB lifecycle
• Added migration support
Slick
class Coffees(tag: Tag) extends Table[(String, Double)](tag, "COFFEES") { def name = column[String]("COF_NAME", O.PrimaryKey) def price = column[Double]("PRICE") def * = (name, price) } val coffees = TableQuery[Coffees] coffees.map(_.name) coffees.filter(_.price < 10.0) val coffeeNames: Seq[Double] = coffees.map(_.price).list coffees.filter(_.price < 10.0).sortBy(_.name).map(_.name)
• Access to the infrastructure Google provide
• https://www.buzzingandroid.com/2013/01/
push-messages-on-android-and-iphone/
Main Activity
final class MainActivity extends OomphActivity with Messaging { lazy val onInstall = new OnInstall(this) val onError: HttpFailure => Unit = { v => Log.i("Oomph", v.toString)} val initializer = config.isStandalone ? standalone | library onCreate(OomphDatabase.migrate(this)) onCreate(setContentView(R.layout.main)) onCreate(initializer.configure(this, config)) onStart(register()) def register() { val installDetails = onInstall.installationDetails() registerForGCM { // register -‐ see later } } }
List Activity
final class LibraryListActivity extends OomphActivity with OomphDatabase.ActivityDB { onCreate({ val fragmentManager = getFragmentManager val fragmentTransaction = fragmentManager.beginTransaction fragmentTransaction.add(android.R.id.content, new AllIssues, "") fragmentTransaction.commit }) onStart({startService( new Intent(this, classOf[GoAndGetEmTigerService]))}) }
Issue Viewer Activityfinal class IssueViewerActivity extends OomphActivity { lazy val oomphUi: WebView = findView(TR.oomphUi) var startUrl: String = _ onCreate { startUrl = getIntent.getExtras.getString("start_url") setContentView(R.layout.main) if (!config.isRelease) { List(TR.refresh_button, TR.select_oomph_issues_list) foreach (b => Option(findView(b)).map(_.setVisibility(View.VISIBLE))) } OomphUi.setup(this, oomphUi, getWindowManager) oomphUi.loadUrl(startUrl) } def webViewReload(view: View) { oomphUi.loadUrl(startUrl) } def selectLibraryList(view: View) { startActivity(new Intent(this, classOf[LibraryListActivity])) } }
JSON Parsing
case class Installation(uuid: String) object Installation { implicit val codec = casecodec1(Installation.apply, Installation.unapply)("uuid") } case class ApprovedDownload(productId: String, url: String, md5: String) object ApprovedDownload { implicit val codec = { val c = casecodec3(ApprovedDownload.apply, ApprovedDownload.unapply) c("productId", "url", “md5") } }
API CALLINGcase class OomphApi(client: OomphClient) { def register(registrationId: String, uuid: String)(f: ApiResponder[Unit]) { client.post("register", Map("registration_id" -‐> registrationId, "device_uuid" -‐> uuid), f) } def issues(f: ApiResponder[List[RemoteIssue]]) { implicit val decodeI = casecodec4(RemoteIssue, RemoteIssue.unapply)( "product_id", "name", "cover_url", "content_md5") client.get("issues", Map.empty, f) } def download(productId: String)(f: ApiResponder[ApprovedDownload]) { val decoder = jdecode2L((url: String, md5: String) => ApprovedDownload(productId, url,md5))("url", "md5") client.post("download", Map("product_id" -‐> productId), f)(decoder) } }
Fetch & Store Dataobject GoAndGetEmTiger { def go[A](o: OomphApi, database: ODB, f: () => Unit) { o.issues(ApiResponder(_ => (), { issues => database.runSession(install(issues)) f() })) } def install(issues: List[RemoteIssue]): DB[Unit] = for { _ <-‐ Issues.markAllInactive _ <-‐ issues.traverse_(recordOrUpdate) } yield () def recordOrUpdate(newIssue: RemoteIssue): DB[Unit] = DB { implicit s => val byProductQuery = for { i <-‐ Issues if i.productId === newIssue.productId } yield i.nextMd5 ~ i.title ~ i.imageUrl ~ i.active if (Query(byProductQuery.length).first > 0) { byProductQuery.update((newIssue.md5, newIssue.title, newIssue.coverUrl, true)) } else { Issues.insert(newIssue.asIssue) } } }
Futuresdef copyContent(context: Activity): Unit = { val dialog = ProgressDialog.show(context, "", "Installing...", true) val r = future {ContentHelper.assetsToInternalStore(context)} r onComplete { v => v match { case TryFailure(e) => e.printStackTrace() case _ => () } context.runOnUiThread { dialog.dismiss() val next = new Intent(context, classOf[IssueViewerActivity]) next.putExtra(“start_url", "file://" + context.getFilesDir + "/content/index.html") context.startActivity(next) context.finish() } } }
GCMtrait Messaging { self: Context with Configured => def registerForGCM(f: Throwable \/ String => Unit) { val gcm = GoogleCloudMessaging.getInstance(this) val r = future { gcm.register(config.gcmSenderId) } r onSuccess { case v => f(\/-‐(v)) } r onFailure { case e => f(-‐\/(e)) } } } val onError: HttpFailure => Unit = { v => Log.e("Oomph", v.toString)} registerForGCM { case -‐\/(e) => Log.e("Oomph", "Registration failed: %s" format e) case \/-‐(v) => { val r = config.oomphApi.register(v, installDetails.uuid) r(ApiResponder(onError)) { _ => Log.i("Oomph", "Registration happened”) }) } }
DB Mechanics - Migrations
object OomphDatabase extends AndroidDatabaseAccess { val databaseName = "oomph" val migrations: Migrations = Migrations(NonEmptyList( Migration(1, """CREATE TABLE issues ( _id INTEGER PRIMARY KEY AUTOINCREMENT, product_id TEXT NOT NULL, title TEXT NOT NULL, image_url TEXT NOT NULL, installed_md5 TEXT, next_md5 TEXT, active BIT DEFAULT 0 ); """ ) )) }
Monadic DB
case class DB[A](f: Session => A) { def map[B](ff: A => B): DB[B] = DB(s => ff(f(s))) def flatMap[B](ff: A => DB[B]): DB[B] = DB(s => ff(f(s)).f(s)) } object DB { implicit def instances: Applicative[DB] = new Applicative[DB] { def point[A](a: => A): DB[A] = DB(_ => a) def ap[A, B](fa: => DB[A])(f: => DB[A => B]): DB[B] = DB(function1Covariant.ap(fa.f)(f.f)) } }
Android Database Integration
object Database { def open(databaseName: String, context: Context): Database = { val path = context.getDatabasePath(databaseName).getCanonicalPath val url = "jdbc:sqldroid:%s".format(path) val database = SlickDatabase.forDriver(new SQLDroidDriver(), url) new Database { def runSession[T](f: DB[T]): T = database.withSession(f.f) def runTransaction[T](f: DB[T]): T = database.withTransaction(f.f) } } }
DB Mechanicstrait AndroidDatabaseAccess { val databaseName: String val migrations: Migrations def migrate(c: Context) { Migrator.migrate(databaseName, migrations, c) } trait ActivityDB extends Activity with DBState { abstract override def onCreate(b: Bundle) { super.onCreate(b) db = Database.open(databaseName, this) } } trait IntentDB extends IntentService with DBState { // ... } trait FragmentDB extends Fragment with DBState { // ... } }
Entity DefInition
case class RemoteIssue(productId: String, title: String, coverUrl: String, md5: String) { def asIssue: Issue = Issue(productId, title, coverUrl, None, md5, true, None) } case class Issue(productId: String, title: String, imageUrl: String, installedMd5: Option[String], nextMd5: String, active: Boolean, id: Option[Int]) object Issues extends Table[Issue]("ISSUES") { def id = column[Int]("_ID", O.PrimaryKey, O.AutoInc, O.NotNull) def productId = column[String]("PRODUCT_ID", O.NotNull) def title = column[String]("TITLE", O.NotNull) def imageUrl = column[String]("IMAGE_URL", O.NotNull) def installedMd5 = column[Option[String]]("INSTALLED_MD5") def nextMd5 = column[String]("NEXT_MD5") def active = column[Boolean]("ACTIVE", O.Default(false)) def baseProjection = productId ~ title ~ imageUrl ~ installedMd5 ~ nextMd5 ~ active def * = baseProjection ~ id.? <>(Issue, Issue.unapply _) def markAllInactive: DB[Unit] = DB { implicit s => this.map(_.active).update(false) } }
DB Usage
class AllIssues extends LibraryListFragment { def query: DB[List[Issue]] = Library.allIssues } class InstalledIssues extends LibraryListFragment { def query: DB[List[Issue]] = Library.installed }
DB Usage - Readobject Library { def allIssues: DB[List[Issue]] = DB { implicit s => val q2 = for { i <-‐ Issues } yield i q2.list } def issueForProductId(pId: String): DB[Issue] = DB { implicit s => val q2 = for { i <-‐ Issues if i.productId === pId } yield i q2.first() } def installed: DB[List[Issue]] = DB { implicit s => val q2 = for { i <-‐ Issues if i.installedMd5 === i.nextMd5 } yield i q2.list } }
DB Access - Write
object Library { def insertIssues(issues: List[Issue]): DB[Unit] = DB { implicit s => issues.map(i => Issues.insert(i)) } def updateIssueNextMd5(issuePid: String, nextMd5: String): DB[Unit] = { DB { implicit s => val q = for {i <-‐ Issues if i.productId === issuePid} yield i.nextMd5 q.update(nextMd5) } } def updateIssueInstalledMd5(issuePid: String, installedMd5: String): DB[Unit] = { DB { implicit s => val q = for {i <-‐ Issues if i.productId === issuePid} yield i.installedMd5 q.update(Some(installedMd5)) } } }
SBT - The Promise
// build.sbt name := "hello" version := "1.0" scalaVersion := “2.10.3" !!// Hi.scala object Hi { def main(args: Array[String]) = println("Hi!") }
SBT - The Realityobject OomphAndroidBuild extends Build { lazy val baseSettings = Defaults.defaultSettings ++ org.scalastyle.sbt.ScalastylePlugin.Settings ++ Seq( scalaVersion := "2.10.1", version := "1.0.4", versionCode := 5, platformName in Android := "android-‐17", buildToolsVersion in Android := "18.1.1", scalacOptions ++= Seq(...), commands ++= Seq(Command.command("ci", Help.empty)(s => Seq("...") ::: s)) ) ++ addCommandAlias("compile-‐and-‐check", ";app/compile;app/scalastyle") val signingSettings = Seq( keyalias in Android := "oomph", cachePasswords in Android := true, keystorePath in Android <<= baseDirectory(_ / "etc" / "keystore") ) val proguardSettings = ... lazy val appSettings = baseSettings ++ AndroidProject.androidSettings ++ proguardSettings ++ TypedResources.settings ++ AndroidManifestGenerator.settings ++ AndroidMarketPublish.settings ++ AndroidManifestGenerator.settings ++ signingSettings ++ Seq( name := "Oomph Android", installEmulator in Android ~= { _ => Seq("adb", "-‐e", "shell", "touch /data/data/com.oomphhq.android/i_am_an_emulator") ! } ) lazy val testsSettings = baseSettings ++ AndroidTest.androidSettings ++ proguardSettings ++ AndroidManifestGenerator.settings ++ Seq( name := "Tests", proguardInJars in Android := Seq(), // wot! libraryDependencies ++= Seq(...) ) lazy val app = Project("app", file("."), settings = appSettings) lazy val tests = Project("tests", file("tests"), settings = testsSettings) dependsOn (app % "provided") }
Little BUILD CHAIN Hacks
• sbt plugin to fix proguard
• Library jars not being included in binary!
• Custom JRE (script) in IntelliJ to fix memory issues (fixed
now)
• Double up of libraries “provided” scope [1] & [2]
• Build tools in v19 has dex bug
[1] https://github.com/jberkel/android-plugin/pull/177
[2] https://groups.google.com/forum/?fromgroups=#!topic/scala-on-android/OuGYJtvQdZo
No Migration Support
• Migration support not built in
• Some Scala support (Java?)
• You will probably need to roll your own
• Moderately difficult to integrate Slick with
Android DB with migrations
ToolChain
• The toolchain described above isn’t (wasn’t?)
well supported
• Many moving parts; IDE, Android, compiler,
plugin
• Not as simple as iOS, normal for JVM people
• However, plenty of support for Java…
Learning
• Scala has a moderately high learning curve
• Quite a complicated language, syntactically &
conceptually
• Lots of ways to do the same thing, e.g. _
• 3 kinds of Scala; Java, “idiomatic”, FP
FP
• Functional patterns can make learning Scala
harder
• Can be hard to fit FP concepts Android;
immutability, (lack of) subclass inheritance
Compiler
• Compiler bugs, much better now
• Compiler speed, though many work arounds &
sbt’s incremental compiler quite decent
Still in Kansas
• You may have a nicer language, but it’s still
Android
• Stub classes when testing
• Same device compatibility issues
• Lack of standardisation, storage paths
• Binary size restrictions
Implicits
• Deceptively simple
• People seem to suffer horrendously with them
• Incredibly hard to debug
• Tip: Use them sparingly, apply some rules
• Bijection
• Import them close to the call site, keep scope small
JDK8
[info] Compiling 1 Scala source to /Users/tom/Projects/Oomph/oomph-‐android/project/project/target/scala-‐2.9.2/sbt-‐0.12/classes... [error] error while loading CharSequence, class file '/Library/Java/JavaVirtualMachines/jdk1.8.0_05.jdk/Contents/Home/jre/lib/rt.jar(java/lang/CharSequence.class)' is broken [error] (bad constant pool tag 18 at byte 10) [error] error while loading Comparator, class file '/Library/Java/JavaVirtualMachines/jdk1.8.0_05.jdk/Contents/Home/jre/lib/rt.jar(java/util/Comparator.class)' is broken [error] (bad constant pool tag 18 at byte 20) [error] two errors found [error] (compile:compile) Compilation failed
Proguard is you’re friend (?)
• Heavily optimise proguard to not hit (class) limits
• Hard to know if you’ve removed too much
• Don’t know if binary runs, classes missing!
• Hard to exercise all code paths
• Automated tests, QA team
• Static analysis?
Proguardval proguardSettings = Seq( useProguard in Android := true, proguardOption in Android := """ |-‐keep public class scala.Function1 |-‐keep public class scala.reflect.ScalaSignature |-‐keep class * extends java.util.ListResourceBundle { | protected Object[][] getContents(); |} |-‐keepclassmembers enum * { | public static **[] values(); | public static ** valueOf(java.lang.String); |} |-‐keepclassmembers class scala.collection.** { | *; |} |-‐keep public class scala.math.Ordering |-‐keepclassmembers class com.oomphhq.whiplash.** { | *; |} |-‐keep public class scala.slick.lifted.DDL { *; } """.stripMargin )
Webviews
• Well…
• Webviews are webviews
• Scala can’t help us here
• Still bad support across devices
Learn FP
• Use Scala as a vehicle to learn FP
• Can share concepts; iOS (Swift), Android (Scala), Web (Elm,
TypeScript, PureScript, Roy)
• Gradual learning curve
• Start with Scala syntax
• Use current programming style
• Gradually introduce FP concepts
• Read lots; FP in Scala, LYAH, “reactive”, etc.
Invest in toolchain
• The toolchain is ordinary, but
• The toolchain is critical
• Invest in it
• Become build experts
(F)RP• (Functional) reactive programming is all the rage
• Promises easy composability
• https://github.com/ReactiveX/RxJava, & on Android [1, 3]
• Good introduction to FRP [2]
[1] http://mttkay.github.io/blog/2013/08/25/functional-reactive-programming-on-android-with-rxjava/
[2] https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
[3] https://github.com/andrewhr/rxjava-android-example
// send button enabled when we have a message messageBodyText.map {!_.trim().equals("") }. subscribe(Properties.enabledFrom(message));
Roboelectric
• Run tests locally rather than on device/emulator
• Implementations of Android SDK
• No mocking
• Java-based API
• JUnit runner
https://github.com/robolectric/robolectric
Robospecs
• Roboelectric for Specs2
https://github.com/jbrechtel/robospecs
class MainActivitySpecs extends RoboSpecs with Mockito { "clicking the showMessageButton" should { "show a toast popup with text from the message input field" in { val activity = new MainActivity() activity.onCreate(null) activity.messageEditText.setText("expected message") activity.showMessageButton.performClick() ShadowToast.getTextOfLatestToast must beEqualTo(“...”) } } }
Misc…
• Can pre-load Scala onto rooted devices [1]
• Android SBT plugin is built into IntelliJ 14 [2, 3]
[1] https://github.com/jbrechtel/Android-Scala-Installer [2] https://github.com/JetBrains/sbt-structure/pull/5
[3] http://youtrack.jetbrains.com/issue/SCL-6273
Fork
• Paul Phillips https://github.com/paulp/policy/blob/
master/README.md
• Comments https://news.ycombinator.com/item?
id=8276565
• TypeLevel http://typelevel.org/blog/2014/09/02/
typelevel-scala.html
• Watch Paul’s talk: https://www.youtube.com/watch?
v=TS1lpKBMkgg
References
• Scala my Android: http://ktoso.github.io/scala-
android-presentation/