Scala on Android
Tom Adams @tomjadams
Scala on Android Tom Adams @tomjadams Me SVP of Polyglot - - PowerPoint PPT Presentation
Scala on Android Tom Adams @tomjadams 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
Tom Adams @tomjadams
inference, no semi-colons, no ‘.’
classes, implicits, packages, mixins, currying/partial application, etc.
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(); ¡ ¡ ¡} ¡ });
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() ¡ }
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);
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)
val ¡button ¡= ¡new ¡Button(context) ¡ button.setText("Greet") ¡ button.setOnClickListener(Toast.makeText( ¡ ¡ ¡ ¡ ¡context, ¡"Hello!", ¡Toast.LENGTH_SHORT).show()) ¡ layout.addView(button)
SButton("Greet", ¡toast("Hello!"))
[1] https://github.com/pfn/android-sdk-plugin
support, signing, tests, etc.
etc.
[1] https://github.com/pfn/android-sdk-plugin [2] https://github.com/pocorall/hello-scaloid-sbt
var ¡connectivityListener: ¡BroadcastReceiver ¡= ¡null ¡
¡ ¡super.onResume() ¡ ¡ ¡connectivityListener ¡= ¡new ¡BroadcastReceiver ¡{ ¡ ¡ ¡ ¡ ¡def ¡onReceive(context: ¡Context, ¡intent: ¡Intent) ¡{ ¡ ¡ ¡ ¡ ¡ ¡doSomething() ¡ ¡ ¡ ¡ ¡} ¡ ¡ ¡} ¡ ¡ ¡ ¡registerReceiver(connectivityListener, ¡ ¡ ¡ ¡ ¡ ¡ ¡new ¡IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)) ¡ } ¡
¡ ¡unregisterReceiver(connectivityListener) ¡ ¡ ¡super.onPause() ¡ }
broadcastReceiver(ConnectivityManager.CONNECTIVITY_ACTION) ¡{ ¡ ¡ ¡(context, ¡intent) ¡=> ¡doSomething() ¡ }
new ¡AsyncTask[String, ¡Void, ¡String] ¡{ ¡ ¡ ¡def ¡doInBackground(params: ¡Array[String]) ¡= ¡{ ¡ ¡ ¡ ¡ ¡doAJobTakeSomeTime(params) ¡ ¡ ¡} ¡
¡ ¡ ¡ ¡alert("Done!", ¡result) ¡ ¡ ¡} ¡ }.execute("param")
Future ¡{ ¡ ¡ ¡val ¡result ¡= ¡doAJobTakeSomeTime(params) ¡ ¡ ¡runOnUiThread(alert("Done!", ¡result)) ¡ }
functional programming
ListOps, etc.)
(ad-hoc polymorphism, traits + implicits)
val ¡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))
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") ¡ ¡ ¡ ¡ ¡} ¡ ¡ ¡} ¡ }
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)
push-messages-on-android-and-iphone/
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 ¡ ¡ ¡ ¡ ¡} ¡ ¡ ¡} ¡ }
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]))}) ¡ }
final ¡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])) ¡ ¡ ¡} ¡ }
case ¡class ¡Installation(uuid: ¡String) ¡ ¡ ¡
¡ ¡implicit ¡val ¡codec ¡= ¡casecodec1(Installation.apply, ¡Installation.unapply)("uuid") ¡ } ¡ ¡ ¡ case ¡class ¡ApprovedDownload(productId: ¡String, ¡url: ¡String, ¡md5: ¡String) ¡ ¡ ¡
¡ ¡implicit ¡val ¡codec ¡= ¡{ ¡ ¡ ¡ ¡ ¡val ¡c ¡= ¡casecodec3(ApprovedDownload.apply, ¡ApprovedDownload.unapply) ¡ ¡ ¡ ¡ ¡c("productId", ¡"url", ¡“md5") ¡ ¡ ¡} ¡ }
case ¡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) ¡ ¡ ¡} ¡ }
¡ ¡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) ¡ ¡ ¡ ¡ ¡} ¡ ¡ ¡} ¡ }
def ¡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() ¡ ¡ ¡ ¡ ¡} ¡ ¡ ¡} ¡ }
trait ¡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”) ¡ ¡ ¡ ¡ ¡}) ¡ ¡ ¡} ¡ }
¡ ¡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 ¡ ¡ ¡ ¡ ¡ ¡ ¡ ¡ ¡ ¡); ¡ ¡ ¡ ¡ ¡ ¡ ¡""" ¡ ¡ ¡ ¡ ¡) ¡ ¡ ¡)) ¡ }
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)) ¡ } ¡ ¡ ¡
¡ ¡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)) ¡ ¡ ¡} ¡ }
¡ ¡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) ¡ ¡ ¡ ¡ ¡} ¡ ¡ ¡} ¡ }
trait ¡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 ¡{ ¡ ¡ ¡ ¡ ¡// ¡... ¡ ¡ ¡} ¡ }
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]) ¡ ¡ ¡
¡ ¡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) ¡} ¡ }
class ¡AllIssues ¡extends ¡LibraryListFragment ¡{ ¡ ¡ ¡def ¡query: ¡DB[List[Issue]] ¡= ¡Library.allIssues ¡ } ¡ ¡ ¡ class ¡InstalledIssues ¡extends ¡LibraryListFragment ¡{ ¡ ¡ ¡def ¡query: ¡DB[List[Issue]] ¡= ¡Library.installed ¡ }
¡ ¡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 ¡ ¡ ¡ ¡ ¡} ¡ }
¡ ¡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)) ¡ ¡ ¡ ¡ ¡} ¡ ¡ ¡} ¡ }
// ¡build.sbt ¡ name ¡:= ¡"hello" ¡ version ¡:= ¡"1.0" ¡ scalaVersion ¡:= ¡“2.10.3" ¡
¡ ¡def ¡main(args: ¡Array[String]) ¡= ¡println("Hi!") ¡ }
¡ ¡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") ¡ }
now)
[1] https://github.com/jberkel/android-plugin/pull/177 [2] https://groups.google.com/forum/?fromgroups=#!topic/scala-
Android DB with migrations
well supported
plugin
conceptually
harder
immutability, (lack of) subclass inheritance
sbt’s incremental compiler quite decent
Android
[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
$ ¡export ¡JAVA_HOME=/Library/Java/Home ¡ $ ¡sbt
val ¡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 ¡ )
TypeScript, PureScript, Roy)
[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));
https://github.com/robolectric/robolectric
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(“...”) ¡ ¡ ¡ ¡ ¡} ¡ ¡ ¡} ¡ }
[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
master/README.md
id=8276565
typelevel-scala.html
v=TS1lpKBMkgg
android-presentation/
Tom Adams @tomjadams