- PVSM.RU - https://www.pvsm.ru -
Так как на работе я пишу на старой доброй Enterprise Java, меня периодически тянет попробовать что-то новое и интересное. Так получилось, что в один момент этим новым и интересным оказалась Scala. И вот однажды, просматривая доклады со Scala Days, я наткнулся на доклад Ника Станченко о библиотеке под названием Macroid, которую он написал. В этой статье я попробую написать маленькое приложение для демонстрации её возможностей и рассказать об основных фишках этой библиотеки. Код приложения целиком доступен на Github [1].
Если вам захотелось узнать, как эта библиотека помогает подружить Scala и Android, добро пожаловать под кат.
Macroid — это DSL на Scala макросах для работы с Android-интерфейсом. Она позволяет избавиться от традиционных проблем XML разметки, таких как:
Macroid позволяет описывать разметку на Scala и делать это там, где удобно. А так же:
Ну и конечно, все плюсы Scala тоже здесь: паттерн-матчинг, функции высшего порядка, трейты, кейс классы, автоматический вывод типов и так далее.
Первым делом нам нужно настроить окружение для разработки под Android. Процедура подробно описана на developer.android.com [2], так что тут её приводить особого смысла нет.
После того как окружение настроено, создаём SBT проект и добавляем android-sbt-plugin.
import android.Keys._
//задаём версию Android
android.Plugin.androidBuild
platformTarget in Android := "android-23"
packagingOptions in Android := PackagingOptions(
Seq.empty[String],
Seq("reference.conf"),
Seq.empty[String])
name := "macroid-for-habr"
scalaVersion := "2.11.7"
javacOptions ++= Seq("-target", "1.7", "-source", "1.7")
// упростим команду сборки чтобы не писать каждый раз Android:run
run <<= run in Android
resolvers ++= Seq(
Resolver.sonatypeRepo("releases"),
"jcenter" at "http://jcenter.bintray.com"
)
// добавим linter
scalacOptions in (Compile, compile) ++=
(dependencyClasspath in Compile).value.files.map("-P:wartremover:cp:" + _.toURI.toURL) ++
Seq("-P:wartremover:traverser:macroid.warts.CheckUi")
libraryDependencies ++= Seq(
aar("org.macroid" %% "macroid" % "2.0.0-M4"),
"com.android.support" % "support-v4" % "23.1.1",
compilerPlugin("org.brianmckenna" %% "wartremover" % "0.11")
)
// Включим proguard для удаления неиспользуемого кода из библиотек при сборке
proguardScala in Android := true
addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.5.13")
Добавляем простенький 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>
Осталось лишь создать класс MainActivity и подключить трейт с контекстами:
class MainActivity extends Activity with Contexts[Activity]
Получившийся проект доступен по тегу Step1 [3].
Интерфейс в Macroid складывается при помощи «Brick». Brick — это кирпичик интерфейса, представляющий собой разметку или отдельный виджет. Разметка обозначается как «layout» или просто «l», а виджеты — «widget» или «w». Например, простой LinearLayout с текстовым полем и кнопкой будет выглядеть вот так:
l[LinearLayout](
w[TextView],
w[Button]
)
Ничего не мешает присваивать такие куски разметки переменным и компоновать их друг с другом там и тогда, когда это требуется:
val view = l[LinearLayout](
w[TextView],
w[Button]
)
Конечно же, виджеты нужно настроить, прописать им какие-то свойства и значения, иначе в них нет никакого смысла.
В этом помогает штука под названием Tweak. Для краткости твики обозначаются оператором <~. К примеру, можно выставить текст полю и кнопке:
w[TextView] <~ text("Просто надпись"),
w[Button] <~ text("Нажми меня")
Также можно для точности прописать, что наш виджет обязательно должен быть вертикальным, при помощи твика vertical:
val view = l[LinearLayout](
w[TextView] <~ text("Просто надпись"),
w[Button] <~ text("Нажми меня")
) <~ vertical
Наконец, какая же кнопка без onClickListener. К примеру, можно изменить текст в поле по нажатию кнопки:
w[Button] <~ text("Нажми меня") <~ On.click(changeText)
Специальный макрос On, попытается вывести имя Listener из того, что написано после точки, и найти его у виджета, к которому он применён. В нашем случае, он попытается найти onClickListener у виджета Button и прописать в нём функцию changeText.
Для того чтобы поменять текст, нужно будет как-то передать виджет поля в функцию changeText. В этом нам поможет метод slot, который оборачивает виджет в Option, что позволяет безопасно привязать результат к переменной и использовать его в коде.
var textView = slot[TextView]
Конкретный виджет привязывается к слоту при помощи твика wire:
w[TextView] <~ wire(textView)
Теперь можно вернуться к написанию метода changeText:
def changeText : Ui[Any] = {
textView <~ text("Другая надпись")
}
Метод состоит из одного твика и возвращает тот самый Ui Action, представляющий собой действие, которое будет выполнено в потоке UI.
Для того чтобы полученная разметка применилась к MainActivity, вызовем setContentView(getUi(view)) в методе onCreate. Метод getUi(view) выполнит UI код в текущем потоке и вернёт нам получившийся View, который мы и установим в ContentView нашей Activity.
Получившийся код доступен по тегу Step2 [4].
В рамках одного Ui действия можно соединить несколько твиков, действующих на разные виджеты. В таком случае твики будут запускаться последовательно друг за другом.
А что делать, если нужно запустить несколько изменений интерфейса одновременно? Тут поможет альтернатива твику, которая называется Snail и обозначается оператором <~~. Snail работает по принципу «выстрелил и забыл». К примеру, можно запустить анимацию затухания для текстового поля:
textView <~~ fadeOut(500)
Для последовательного объединения нескольких Snail в один, применяется оператор ++. Вот так может выглядеть Snail для мигания любым View:
def flashElement : Snail[View] = {
fadeOut(500) ++ fadeIn(500) ++ fadeOut(500) ++ fadeIn(500)
}
С твиками можно проделывать аналогичные действия при помощи оператора +.
Добавим небольшой вложенный кусок линейной разметки с ещё одним текстовым полем и привяжем его к слоту:
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
А теперь дадим нашей кнопке больше влияния на окружающее:
def changeText : Ui[Any] = {
(textView <~ text("Помигаем?")) ~ (layout <~~ flashElement) ~~ (textView <~ text("Ну и хватит"))
}
Теперь по нажатию на кнопку в первом поле сменится текст и одновременно с этим наш новенький кусок разметки начнёт мигать, а после окончания анимации текст изменится ещё раз. Достигается это при помощи двух операторов ~ и ~~. Первый оператор запускает оба действия одновременно, а второй, запустит следующее действие только после окончания предыдущего.
Получившийся код доступен по тегу Step3 [5].
Наверняка при написании приложения нам понадобится вывести какой-нибудь список. Посмотрим, как Macroid управляется с этим.
Если требуется создать простой список, мы можем воспользоваться Listable. Трейт Listable[A, W <: View] указывает, как именно должен отображаться объект типа A при помощи виджета W, и делается это в два простых шага:
Добавим метод basicListable, который будет создавать пустой TextView, заполнять его текстом при помощи твика text() и возвращать нам объект типа Listable[String, TextView]:
def basicListable(implicit appCtx: AppContext): Listable[String, TextView] = {
Listable[String].tw { w[TextView] } { text(_) }
}
Осталось лишь добавить ListView в нашу разметку, применить к нему твик Listable.listAdapterTweak и передать этому твику простенький список из строк:
w[ListView] <~ basicListable.listAdapterTweak(contactList)
Получившийся код доступен по тегу Step4 [6].
Если мы хотим использовать фрагменты в нашем приложении, то и тут Macroid есть что предложить. Для начала нам нужно переделать нашу Activity во FragmentActivity:
class MainActivity extends FragmentActivity
Создадим фрагмент и вынесем туда все, что относится к 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
)
}
Заодно приберёмся немного в коде и вынесем список контактов в отдельный трейт Contacts, а твик flashElement в трейт Tweaks.
Фрагмент вставляется в разметку при помощи «fragment» или «f» и оборачивается во FrameLayout методом framed.
Фрагменту нужны Id и Tag, и для их создания предлагается использовать трейт IdGeneration и классы IdGen и TagGen. Достаточно лишь прописать желаемые tag и id после точки в Tag и Id и передать их методу framed:
f[ListableFragment].framed(Id.contactList, Tag.contactList)
Получившийся код доступен по тегу Step5 [7]
В Android при прокручивании списка с элементами, состоящими из кучки виджетов, можно столкнуться с проблемами производительности из-за того, что адаптер часто использует поиск по id(findViewById()). В качестве средства борьбы с этой проблемой предлагается шаблон View Holder.
В Macroid есть трейт SlottedListable, который воплощает этот шаблон, и для списков со сложными элементами автор библиотеки советует использовать именно его. Для использования этого трейта нужно переопределить один тип и два метода.
Создадим класс ContactListable внутри которого переопределим:
Создадим кейс класс Contact(name:String, phone: String, photo: Int), который представляет собой минималистичный контакт с именем, телефоном и фото.
Создадим метод fullContactList, который вернёт нам список из трёх контактов.
Для того чтобы список контактов был больше похож на настоящий, сделаем фотографии в виде RoundedBitmapDrawable при помощи твика:
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)
})
А также добавим пару твиков для всяческих визуальных улучшений вроде ImageView.ScaleType.CENTER_INSIDE, уменьшающего изображение до нужного размера.
Осталось лишь создать фрагмент SlottedListableFragment, который будет показывать этот список контактов.
Два списка контактов на одном экране это многовато, поэтому я предлагаю заменить старый ListableFragment, на новенький и блестящий SlottedListableFragment. Для этого напишем новый твик, заменяющий один фрагмент, используя трейт FragmentApi, любезно предоставленный Macroid:
def replaceListableWithSlotted = Ui {
activityManager(this).beginTransaction().replace(Id.contactList,new SlottedListableFragment,Tag.slottedList).commit()
}
А затем добавим этот твик в конец нашего многострадального метода changeText, который теперь будет называться changeTextAndShowFragment.
Получившийся код доступен по тегу Step6 [8].
Говоря о Scala, нельзя обойти стороной акторы и библиотеку Akka.
Macroid предлагает использовать акторы для передачи сообщений между фрагментами и предоставляет для этого трейт AkkaFragment. На каждый AkkaFragment создаётся свой актор. Акторы живут пока живёт Activity, фрагменты же живут в своём обычном жизненном цикле. Таким образом, акторы могут присоединяться к контролируемому ими интерфейсу и отделяться от него.
Для начала необходимо добавить зависимости macroid-akka и akka-actor в build.sbt. А также прописать в нём правила для 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 { *; }"
)
Создадим простенький фрагмент с одной кнопкой, цвет текста в которой можно изменить, вызвав метод receiveColor. Имя актора будет передаваться в этот фрагмент при помощи аргументов:
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)
)
}
}
Создадим актор, который будет управлять этим фрагментом:
class TweakerActor(var tweakTarget:Option[ActorRef]) extends FragmentActor[TweakerFragment]{
import TweakerActor._
// receiveUi обрабатывает отсоединение/присоединение к интерфейсу. После этого мы можем делать все что хотим
def receive = receiveUi andThen {
case TweakHim => tweakTarget.foreach(_ ! TweakYou)
case TweakYou =>
val chosenColor = randomColor
tweakTarget = Some(sender)
//withUi предоставляет нам возможность взаимодействовать с управляемым фрагментом
withUi(f => f.receiveColor(chosenColor))
case SetTweaked(target) => tweakTarget = Some(target)
// можно добавить своё поведение для присоединения/отсоединения в дополнение к стандартному
case AttachUi(_) =>
case DetachUi =>
}
def randomColor: Int = {
val random = new Random()
val red = random.nextInt(255)
val green = random.nextInt(255)
val blue = random.nextInt(255)
Color.rgb(red, green, blue)
}
}
В MainActivity добавим трейт AkkaActivity. Добавим переменные для пары одинаковых акторов и системы:
val actorSystemName = "tutorialsystem"
lazy val tweakerOne = actorSystem.actorOf(TweakerActor.props(None), "tweakerOne")
lazy val tweakerTwo = actorSystem.actorOf(TweakerActor.props(Some(tweakerOne)), "tweakerTwo")
Инициализируем акторы в методе onCreate и добавим фрагменты для акторов в разметку:
l[LinearLayout](
f[TweakerFragment].pass("name" -> "tweakerOne").framed(Id.tweakerOne, Tag.tweakerOne),
f[TweakerFragment].pass("name" -> "tweakerTwo").framed(Id.tweakerTwo, Tag.tweakerTwo)
) <~ horizontal
В методе onStart передадим первому актору сообщение, а в onDestroy пропишем закрытие системы:
override def onStart() = {
super.onStart()
tweakerOne ! TweakerActor.SetTweaked(tweakerTwo)
}
override def onDestroy() = {
actorSystem.shutdown()
}
В результате мы получаем два фрагмента с кнопками, которые могут скомандовать друг другу сменить цвет текста.
Получившийся код доступен по тегу Step7 [9].
На этом я заканчиваю свою статью. Конечно, какие-то возможности Macroid остались за кадром, но я надеюсь, что мне удалось пробудить желание попробовать на зуб эту библиотеку или хотя бы присмотреться к ней. В данный момент библиотека развивается ребятами из команды 47 Degrees и доступна на Github [10].
Спасибо за внимание.
Автор: cheshirrrr
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android/115865
Ссылки в тексте:
[1] Github: https://github.com/cheshirrrr/macroid-for-habr
[2] developer.android.com: http://developer.android.com/intl/ru/training/basics/firstapp/index.html
[3] Step1: https://github.com/cheshirrrr/macroid-for-habr/tree/Step1
[4] Step2: https://github.com/cheshirrrr/macroid-for-habr/tree/Step2
[5] Step3: https://github.com/cheshirrrr/macroid-for-habr/tree/Step3
[6] Step4: https://github.com/cheshirrrr/macroid-for-habr/tree/Step4
[7] Step5: https://github.com/cheshirrrr/macroid-for-habr/tree/Step5
[8] Step6: https://github.com/cheshirrrr/macroid-for-habr/tree/Step6
[9] Step7: https://github.com/cheshirrrr/macroid-for-habr/tree/Step7
[10] Github: https://github.com/47deg/macroid
[11] Источник: https://habrahabr.ru/post/279973/
Нажмите здесь для печати.