
Since at work I am writing on the good old Enterprise Java, I am periodically tempted to try something new and interesting. It so happened that at one moment Scala turned out to be this new and interesting. Then one day, looking through reports from Scala Days, I came across a report by Nick Stanchenko about a library called Macroid, which he wrote. In this article I will try to write a small application to demonstrate its capabilities and talk about the main features of this library. Application code is entirely available on
Github .
If you wanted to know how this library helps to make friends with Scala and Android, welcome under cat.
What is Macroid?
Macroid is a DSL on Scala macros for working with the Android interface. It gets rid of traditional XML markup problems, such as:
- Lack of namespace, since all markup files are in the same directory;
- Excess files in the project due to the fact that each model must have its own file, as well as due to duplication of models for different screen sizes and other things;
- Even with the markup in the XML file, you still need the code (to assign the same event handlers).
Macroid allows you to describe the markup on Scala and do it where it is convenient. And:
')
- Abstractions are added that allow you to pull out duplicate pieces of the interface code and reuse them.
- Macroid distinguishes AppContext and ActivityContext, stores them separately from each other and passes in the form of implicit values. The ActivityContext is stored as a weak link, thus avoiding memory leak problems.
- Thanks to the UI Action, which wraps any actions with the interface, thread safety is improved, and it is also possible to combine these actions and send them to the UI stream for execution at once.
And of course, all the advantages of Scala are also here: pattern matching, higher order functions, traits, case classes, automatic type inference, and so on.
Proceed to the application
First of all, we need to set up the environment for development for Android. The procedure is described in detail on
developer.android.com , so there is not much point in citing it.
Once the environment is set up, create an SBT project and add the android-sdk-plugin.
plugin.sbt: addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.5.13")
Add a simple AndroidManifest.xml:
AndroidManifest.xml <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="tutorial.macroidforhabr" android:versionCode="0" android:versionName="0.1"> <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="23"/> <application android:label="Macroid for Habr" android:icon="@drawable/android:star_big_on"> <activity android:label="Macroid for Habr" android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest>
It remains only to create the class MainActivity and connect the trait with contexts:
class MainActivity extends Activity with Contexts[Activity]
The resulting project is available on the
Step1 tag.
Let's start with the basics
The interface in Macroid is folded using the “Brick”. A brick is an interface brick representing a markup or a separate widget. The markup is referred to as “layout” or simply “l”, and widgets are referred to as “widget” or “w”. For example, a simple LinearLayout with a text field and a button will look like this:
l[LinearLayout]( w[TextView], w[Button] )
Nothing prevents us from assigning such pieces of markup to variables and linking them together when and where required:
val view = l[LinearLayout]( w[TextView], w[Button] )
Of course, the widgets need to be set up, set some properties and values ​​for them, otherwise there is no point in them.
This helps the thing called Tweak. For brevity, tweaks are denoted by the operator <~. For example, you can set the text to the field and button:
w[TextView] <~ text(" "), w[Button] <~ text(" ")
You can also prescribe for accuracy that our widget must be vertical, with the help of vertical tweaks:
val view = l[LinearLayout]( w[TextView] <~ text(" "), w[Button] <~ text(" ") ) <~ vertical
Finally, what a button without onClickListener. For example, you can change the text in the field by pressing the button:
w[Button] <~ text(" ") <~ On.click(changeText)
The special macro On, will try to deduce the name Listener from what is written after the dot, and find it in the widget to which it is applied. In our case, he will try to find onClickListener from the Button widget and write the changeText function in it.
In order to change the text, you will need to somehow pass the field widget to the changeText function. With this we can use the slot method, which wraps the widget in Option, which allows you to safely bind the result to a variable and use it in code.
var textView = slot[TextView]
A specific widget is attached to the slot with the help of tweak wire:
w[TextView] <~ wire(textView)
Now you can return to writing the method changeText:
def changeText : Ui[Any] = { textView <~ text(" ") }
The method consists of one tweak and returns the same Ui Action, which is an action that will be executed in the UI stream.
In order for the resulting markup to be applied to the MainActivity, call setContentView (getUi (view)) in the onCreate method. The getUi (view) method will execute the UI code in the current thread and return the resulting View to us, which we will set in the ContentView of our Activity.
The resulting code is available on the
Step2 tag.
Change different parts at the same time
Within one Ui action, you can connect several tweaks acting on different widgets. In this case, tweaks will be launched sequentially one after the other.
And what if you need to run several interface changes at the same time? An alternative to tweaks, called Snail, will help here and is denoted by the operator <~~. Snail works on the “shot and forget” principle. For example, you can run a fade animation for a text field:
textView <~~ fadeOut(500)
To sequentially merge several Snails into one, apply the operator ++. This is what Snail can look like for flashing any View:
def flashElement : Snail[View] = { fadeOut(500) ++ fadeIn(500) ++ fadeOut(500) ++ fadeIn(500) }
With tweaks, you can perform similar actions using the + operator.
Add a small nested piece of linear markup with another text field and assign it to the slot:
var layout = slot[LinearLayout] val view = l[LinearLayout]( w[TextView] <~ text(" ") <~ wire(textView), w[Button] <~ text(" ") <~ On.click(changeText), l[LinearLayout]( w[TextView] <~ text(" ") ) <~ wire(layout) ) <~ vertical
Now let's give our button more influence on the environment:
def changeText : Ui[Any] = { (textView <~ text("?")) ~ (layout <~~ flashElement) ~~ (textView <~ text(" ")) }
Now, by pressing the button in the first field, the text will change and at the same time our new piece of markup will flash, and after the end of the animation, the text will change again. This is achieved with the help of two operators ~ and ~~. The first statement starts both actions at the same time, and the second one, starts the next action only after the end of the previous one.
The resulting code is available on the
Step3 tag.
Lists, lists, lists
Surely when writing an application, we will need to display some list. Let's see how Macroid is managed with this.
If you want to create a simple list, we can use Listable. The Listable [A, W <: View] trait indicates exactly how an object of type A should be displayed using the W widget, and this is done in two simple steps:
- Create Empty View
- Fill it with data
Add the basicListable method, which will create an empty TextView, fill it with text using tweak text () and return us an object of type Listable [String, TextView]:
def basicListable(implicit appCtx: AppContext): Listable[String, TextView] = { Listable[String].tw { w[TextView] } { text(_) } }
It remains only to add a ListView to our markup, apply the Listable.listAdapterTweak tweak to it and pass a simple list of strings to this tweak:
w[ListView] <~ basicListable.listAdapterTweak(contactList)
The resulting code is available on the
Step4 tag.
Fragments
If we want to use fragments in our application, then here Macroid has something to offer. First, we need to redo our Activity in FragmentActivity:
class MainActivity extends FragmentActivity
Create a snippet and put there everything related to the ListView:
class ListableFragment extends Fragment with Contexts[Fragment]{ def basicListable(implicit appCtx: AppContext): Listable[String, TextView] = { Listable[String].tw { w[TextView] } { text(_) } } override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View = getUi { l[LinearLayout]( w[ListView] <~ basicListable.listAdapterTweak(contactList) <~ matchWidth ) }
At the same time, let's tidy up a bit in the code and put the contact list into a separate Contacts treyte, and flashElement tweak into the Tweaks treyt.
The fragment is inserted into the markup using a “fragment” or “f” and wrapped in FrameLayout using the framed method.
The fragment needs Id and Tag, and for their creation it is proposed to use the IdGeneration treit and the IdGen and TagGen classes. It is enough to register the desired tag and id after the point in Tag and Id and pass them to the framed method:
f[ListableFragment].framed(Id.contactList, Tag.contactList)
The resulting code is available on the
Step5 tag.
And again lists
On Android, when scrolling through a list with items consisting of a bunch of widgets, you may encounter performance problems due to the fact that the adapter often uses id search (findViewById ()). As a means of dealing with this problem, the View Holder template is proposed.
In Macroid, there is a SlottedListable trait that embodies this template, and for lists with complex elements, the author of the library advises to use it. To use this trait, you need to override one type and two methods.
Create a class ContactListable inside which we redefine:
- class Slots, which is thus the View Holder and contains slots for all necessary View
- the makeSlots method in which the markup is created
- the fillSlots method in which the markup is filled with the values ​​passed
Create a case class Contact (name: String, phone: String, photo: Int), which is a minimalistic contact with the name, phone and photo.
Create a method fullContactList, which will return us a list of three contacts.
In order for the contact list to look more like a real one, let's take photos in the form of a RoundedBitmapDrawable with the help of a tweak:
def roundedDrawable(id: Int)(implicit appCtx: AppContext) = Tweak[ImageView]({ val res = appCtx.app.getResources val rounded = RoundedBitmapDrawableFactory.create(res,BitmapFactory.decodeResource(res, id)) rounded.setCornerRadius(Math.min(rounded.getMinimumWidth(),rounded.getMinimumHeight)) _.setImageDrawable(rounded) })
And also add a couple of tweaks for all sorts of visual improvements like ImageView.ScaleType.CENTER_INSIDE, reducing the image to the desired size.
It remains only to create a fragment SlottedListableFragment, which will show this list of contacts.
Two contact lists on one screen are a bit too much, so I suggest replacing the old ListableFragment with a new and shiny SlottedListableFragment. To do this, we will write a new tweak, replacing one fragment, using the FragmentApi trait, kindly provided by Macroid:
def replaceListableWithSlotted = Ui { activityManager(this).beginTransaction().replace(Id.contactList,new SlottedListableFragment,Tag.slottedList).commit() }
And then add this tweak to the end of our long-suffering changeText method, which will now be called changeTextAndShowFragment.
The resulting code is available on the
Step6 tag.
What is Scala without Akka
Speaking of Scala, you can not ignore the actors and the library Akka.
Macroid suggests using actors to transfer messages between fragments and provides the AkkaFragment trait for this. For each AkkaFragment, an actor is created. Actors live while the Activity lives, while the fragments live in their normal life cycle. Thus, actors can join the interface controlled by them and be separated from it.
First you need to add dependencies macroid-aka and akka-actor to build.sbt. And also write the rules for Proguard in it:
proguard proguardOptions in Android ++= Seq( "-keep class akka.actor.Actor$class { *; }", "-keep class akka.actor.LightArrayRevolverScheduler { *; }", "-keep class akka.actor.LocalActorRefProvider { *; }", "-keep class akka.actor.CreatorFunctionConsumer { *; }", "-keep class akka.actor.TypedCreatorFunctionConsumer { *; }", "-keep class akka.dispatch.BoundedDequeBasedMessageQueueSemantics { *; }", "-keep class akka.dispatch.UnboundedMessageQueueSemantics { *; }", "-keep class akka.dispatch.UnboundedDequeBasedMessageQueueSemantics { *; }", "-keep class akka.dispatch.DequeBasedMessageQueueSemantics { *; }", "-keep class akka.dispatch.MultipleConsumerSemantics { *; }", "-keep class akka.actor.LocalActorRefProvider$Guardian { *; }", "-keep class akka.actor.LocalActorRefProvider$SystemGuardian { *; }", "-keep class akka.dispatch.UnboundedMailbox { *; }", "-keep class akka.actor.DefaultSupervisorStrategy { *; }", "-keep class macroid.akka.AkkaAndroidLogger { *; }", "-keep class akka.event.Logging$LogExt { *; }" )
Let's create a simple fragment with one button, the color of the text in which can be changed by calling the receiveColor method. The name of the actor will be passed to this fragment using arguments:
class TweakerFragment extends AkkaFragment with Contexts[AkkaFragment]{ lazy val actorName = getArguments.getString("name") lazy val actor = Some(actorSystem.actorSelection(s"/user/$actorName")) var button = slot[Button] def receiveColor(textColor: Int) = button <~ color(textColor) def tweak = Ui(actor.foreach(_ ! TweakerActor.TweakHim)) override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle) = getUi { l[FrameLayout]( w[Button] <~ wire(button) <~ text("TweakHim") <~ On.click(tweak) ) } }
Create an actor that will manage this fragment:
class TweakerActor(var tweakTarget:Option[ActorRef]) extends FragmentActor[TweakerFragment]{ import TweakerActor._
Add an AkkaActivity treit to the MainActivity. Add variables for a pair of identical actors and the system:
val actorSystemName = "tutorialsystem" lazy val tweakerOne = actorSystem.actorOf(TweakerActor.props(None), "tweakerOne") lazy val tweakerTwo = actorSystem.actorOf(TweakerActor.props(Some(tweakerOne)), "tweakerTwo")
We initialize the actors in the onCreate method and add fragments for the actors to the markup:
l[LinearLayout]( f[TweakerFragment].pass("name" -> "tweakerOne").framed(Id.tweakerOne, Tag.tweakerOne), f[TweakerFragment].pass("name" -> "tweakerTwo").framed(Id.tweakerTwo, Tag.tweakerTwo) ) <~ horizontal
In the onStart method, we pass the message to the first actor, and in onDestroy we write the system shutdown:
override def onStart() = { super.onStart() tweakerOne ! TweakerActor.SetTweaked(tweakerTwo) } override def onDestroy() = { actorSystem.shutdown() }
As a result, we get two fragments with buttons that can command each other to change the text color.
The resulting code is available on the
Step7 tag.
Conclusion
On this I finish my article. Of course, some features of Macroid were left behind the scenes, but I hope that I managed to awaken the desire to try this library on a tooth or at least look at it. The library is currently being developed by guys from the 47 Degrees team and is available on
Github .
Thanks for attention.