Skip to content

Latest commit

 

History

History
196 lines (149 loc) · 8.08 KB

README.md

File metadata and controls

196 lines (149 loc) · 8.08 KB

#Positronic Net

Positronic Net is an attempt to add some MVC flavor to the process of developing Android apps, and to reduce the amount of boilerplate code required to just connect framework components together. It's written in Scala, and uses Scala features (traits, a/k/a mixins, and "lambdas" or functional arguments) to let programmers say what they mean without so much chatter about components of the framework itself.

It builds using sbt version 0.11.0 and the sbt-android-plugin.

Available documentation includes:

There are also two simple sample apps bundled with the library (as SBT subprojects).

#Features

What's in the package right now includes the following:

##Event handler declarations in a style reminiscent of jQuery:

listsView.onItemClick { (view, posn, id) => viewListAt( posn ) }

findView( TR.addButton ).onClick { doAdd }
findView( TR.newListName ).onKey( KeyEvent.KEYCODE_ENTER ){ doAdd }

onOptionsItemSelected( R.id.undelete ) { doUndelete }

These come from a PositronicHandler trait and friends, which come premixed into PositronicActivity, PositronicButton, and so forth. (These extend the corresponding Android platform classes, so if you don't have a Positronic shorthand for something, or just don't like it, the standard API is fully available.)

##A fairly simple, lightweight ORM

Designing an object-relational mapper for Android means dealing with some unusual constraints. Most notably, it's generally good practice on Android to manipulate databases, and perform other background operations, on a thread different from the one that runs the user interface. That means that a typical ORM API along the lines of, say

myRecord.updateDescription( "... new stuff ..." )
myRecord.save

is problematic. If it does the obvious thing --- save the record, and return to the caller --- and an Activity runs that code on its "UI thread", then the user interface blocks while the save is in progress. That can be enough to create a laggy experience, if other file system activity is going on, and the code has to wait for shared locks. This problem gets even knottier when you consider queries:

val recs = TodoItems.whereEq( "isDone" -> false ).find

If an activity needs the records (say, to display them), and asks for them this way, then it's potentially blocked while the query is going on. But if not, then how does activity code ever load the records' values into its widgets?

Positronic Net's ORM tries to deal with this by borrowing some ideas from Actor-based libraries: We consider activities and the ORM to be concurrent processes, which exchange data via messages. The messages can include ORM-managed records going either way, which are considered to be immutable snapshots of the mutable persistent state that's managed by the ORM --- or requested changes to that state.

###Basic ORM usage

To try to make that clear with a concrete example: Imagine we have TodoItems which have a task description, and a "done" state.

case class TodoItem( todoListId: Long    = ManagedRecord.unsavedId,
                     description: String = null, 
                     isDone: Boolean     = false,
                     id: Long            = ManagedRecord.unsavedId 
                   )
  extends ManagedRecord( TodoItems )
{
  def setDescription( s: String ) = copy( description = s )
  def setDone( b: Boolean )       = copy( isDone = b )
}

object TodoItems extends RecordManager[ TodoItem ]( TodoDb( "todo_items" ))

Here, TodoItems is declared as a singleton RecordManager which is ultimately responsible for managing the persistent storage of things of class TodoItem. An activity could ask it to save one by saying, say:

TodoItems ! Save( oldItem.setDescription( "... new description ..." ))

This arranges for the I/O to happen on a background thread; the activity's main thread can proceed immediately. Queries are a bit more complicated. Here's an example, which also shows a little other machinery:

TodoItems.whereEq( "isDone" -> false ) ! Fetch { recs =>
  for (rec <- recs) ...
}

Here, we're constructing a restricted entity representing a subset of the TodoItems, and sending it the Fetch message, which has a body that is invoked, with the records as an argument, when they are available. (As a special-case favor to the needs of the Android UI, we use an android.os.Handler to run the body of a Fetch request, and similar requests, on the thread that invoked them --- so if an activity does a Fetch on its main UI thread, it's safe to manipulate UI components within the Fetch body.)

###"Change notification" framework

It can still be bothersome to have to explicitly re-query the ORM to refresh displays when relevant state changes. If you have an Activity which manages a list of some kind, you need to update it when items are added or deleted, and perhaps when the state changes otherwise. It's more convenient if the UI components register with the ORM when the list changes, so that a

TodoItems ! Delete( ... )

or

TodoItems ! Save( ... )

makes them update automatically, without further code at the point of the delete and update.

So, Positronic Net provides UI components that do that, for example:

class TodoItemsAdapter( activity: PositronicActivity, 
                        query: Notifier[IndexedSeq[TodoItem]] )
  extends IndexedSeqSourceAdapter( activity,
                                   TodoItems,
                                   itemViewResourceId = R.layout.todo_row )
{
  override def bindView( view: View, it: TodoItem ) =
    view.asInstanceOf[ TodoItemView ].setTodoItem( it )
}

Whenever TodoItems (or one of its subsidiary scopes created by whereEq and friends) gets updated, it will automatically pass an updated sequence to this adapter, which will in turn update any associated views; the activity code that caused the update doesn't have to worry about it.

(Footnote: an activity is passed in to the constructor so the IndexedSeqSourceAdapter can automatically stop watching the TodoItems when the associated activity quits.)

If you're doing something idiosyncratic, the underlying machinery is also available to be used directly, e.g. by AddWatcher and StopWatcher requests.

###One-to-many associations

The ORM also currently has some support for many-to-one and one-to-many associations. An app which manages multiple to-do lists might do something like this: note the HasMany and BelongsTo items:

case class TodoList( name: String = null,
                     id: Long     = ManagedRecord.unsavedId )
  extends ManagedRecord( TodoLists )
{
  def setName( s: String ) = copy( name = s )

  lazy val items = new HasMany( TodoItems )
  lazy val doneItems = items.whereEq( "is_done" -> true )
}

If you have a TodoList, then myList.items will automatically query for the set of TodoItems whose todo_list_id in the database matches the id of the list itself. There's also a BelongsTo class which offers limited support for the one-to-many side of the association (at least enough to be able to Fetch the list associated with an item!)

Full examples of this can be seen by viewing the todo-list sample app in the sample directory; the above has been (slightly) simplified excerpts.

##Miscellaneous shorthands

The library also provides alternate overloads for a lot of standard framework APIs (usually to give them functional arguments instead of, say, a Runnable), which allow common usage patterns to be more clearly (and briefly) expressed. I've also tried to cut down on other forms of chatter --- if you always have to call super.foo(), the Positronic Net alternate version of foo will often do that for you. (Again, if you're doing something unusual, the standard APIs and entrypoints are always available.)