Below are just a few of the motivating ideas that make up the Philosophia Sofia.
Java is a statically-typed language, which imposes certain design patterns and conventions on the programmer. For example, sometimes polymorphism through virtual methods called on a base class reference is not enough to solve a problem; runtime type checks (instanceof
) and clunky static downcasts are needed to get at specific data from an object returned by the system. (For example, think about retrieving a reference to a widget of a specific type by calling findViewById
.)
Likewise, event handling often takes place using listener interfaces, which have their own set of limitations. Consider handling the event that is fired when an item is clicked in a ListView
. Some object in your system must implement the OnItemClickListener
interface. You can do this in the activity class that owns the view, but this forces you to only have one method for all list views that you might have in that activity. Named or anonymous inner classes remove this limitation, but are awkward in other ways.
We have chosen to free ourselves of the shackles above, eschewing tradition when it would only serve to slow down the development process or provide little value in terms of solving an actual task in the problem domain. We strive not only to provide a simpler way for students to write Android apps, but a better way. The framework should handle as much of the "glue" as possible, leaving the programmer to focus much more on the meaningful app logic.
Many software frameworks used to teach introductory computer science courses cause students to hit a ceiling after reaching a certain degree of proficiency, at which point the framework becomes unusable. Many of these educational frameworks support a very restricted subset of functionality and applications developed with them end up looking like "toys" or otherwise fail to meet the standards of real applications on the target platform, which demotivates students.
These concerns were at the front of our minds when we developed Sofia. We don't want students to feel forced into the Sofia philosophy; we want them to find comfort in it and appreciate it on its own. Students who have done Android programming before using Sofia have reacted very positively about the way the framework simplifies and abstracts many of the lower-level aspects of the platform.
By not locking users into a particular programming model, we ensure that it is always possible to fall back to the standard Android API if Sofia doesn't support a feature that students would need. Unlike a lot of other tools and frameworks, Sofia doesn't provide a totally different or alien set of building blocks for constructing Android applications; it just provides bigger blocks. This makes it possible for Sofia to be used not only by students but by professional Android developers as well. Students will learn skills that they can build on after class, not skills that they have to unlearn or replace.
Here are some ways that Sofia applies convention over configuration principles to Android programming:
Task | Using Traditional Android APIs | Using Sofia |
---|---|---|
Loading layouts |
Your activity must explicitly call setContentView to load a layout resource.
|
For subclasses of sofia.app.Screen , the layout resource is loaded automatically based on the name of the class. For example, a class named FooScreen would load res/layout/fooscreen.xml , if found.
|
Accessing widgets |
Your activity must explicitly call findViewById to look up a widget in your layout. This method returns View , so you must then explicitly downcast it to the actual type.
|
For subclasses of sofia.app.Screen , private fields with names that match IDs in the auto-loaded layout will be automatically filled with references to those widgets, as long as the types are compatible.
|
Handling events | Your code must explicitly implement listener interfaces and then call setter methods that register the listeners with the widgets you are interested in. |
Events are dispatched automatically to methods that are named based on the ID of the widget and the type of event you are interested in. For example, myListItemClicked would be called when an item is clicked in a ListView with the ID myList .
|
Imagine that you had an object-oriented graphics framework like the one in Sofia. It has a Shape
class, which then has subclasses for specific kinds of shapes, like RectangleShape
and OvalShape
. Imagine further that when these shapes are on the screen, they can receive notifications when they collide with other shapes. In order to polymorphically handle collisions between all kinds of shapes, a traditional Java library might have the user override a method like this in their Shape
subclass:
public void onCollisionWith(Shape otherShape) { // ... }
But we're not done yet. At this point, we don't know what type of shape the other one actually is. If we're writing a game, we certainly want to know this; collisions between friendly characters would be handled different than collisions between a friendly character and a monster. This means we need to do runtime type checks by hand:
public void onCollisionWith(Shape otherShape) { if (otherShape instanceof GoodGuy) { GoodGuy goodGuy = (GoodGuy) otherShape; // Do something with goodGuy... } else if (otherShape instanceof BadGuy) { BadGuy badGuy = (BadGuy) otherShape; // Do something with badGuy... } }
This kind of boilerplate code doesn't contribute much real value to the solution; in fact, it detracts from the legibility of the solution. Wouldn't it be nice if you could just say what you meant in the first place, like this?
public void onCollisionWith(GoodGuy goodGuy) { // Do something with goodGuy... } public void onCollisionWith(BadGuy badGuy) { // Do something with badGuy... }
Sofia lets you do exactly this! Polymorphism in Java is "single dispatch", meaning that only the type of the object receiving the method call is considered when determining which implementation of the method to call at runtime. The actual types of the objects passed to the method are not considered.
For its internal event dispatch mechanism, Sofia considers the argument types as well. Using reflection, it looks up methods based on the actual types of the arguments, not the compile-time types of the reference. This achieves "multiple dispatch" for these methods, allowing the programmer to focus on their design and letting the framework manage the boilerplate glue.