MacroidでScalaとAndroidを友達にしよう

画像 仕事で古き良きエンタープライズJavaで書いているので、私は定期的に新しい何か面白いことを試みるように描かれています。 たまたまScalaがこの新しい興味深いものであることが判明しました。 そしてある日、Scala Daysのレポートを見て、Nick StanchenkoがMacroidというライブラリについて書いたレポートに出会いました。 この記事では、小さなアプリケーションを作成してその機能を実証し、このライブラリの主な機能について説明します。 アプリケーションコード全体はGithubで入手できます。



このライブラリがScalaとAndroidの友人をどのように支援するかを知りたい場合は、catへようこそ。


マクロイドとは何ですか?



Macroidは、Androidインターフェイスを操作するためのScalaマクロのDSLです。 以下のような従来のXMLマークアップの問題を取り除くことができます。





Macroidを使用すると、Scalaでマークアップを記述し、都合の良い場所で実行できます。 そしてまた:





そしてもちろん、Scalaのすべての利点もここにあります。パターンマッチング、高階関数、特性、ケースクラス、自動型推論などです。



アプリケーションに取りかかりましょう



まず、Androidの開発環境をセットアップする必要があります。 手順はdeveloper.android.comで詳細に説明さているため、ここに持ってくることは意味がありません。



環境を構成したら、SBTプロジェクトを作成し、android-sdk-pluginを追加します。



build.sbt:
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
      
      







plugin.sbt:
 addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.5.13")
      
      







簡単な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>
      
      







MainActivityクラスを作成し、特性をコンテキストに接続するだけです。



 class MainActivity extends Activity with Contexts[Activity]
      
      





結果のプロジェクトは、タグStep1で利用できます。



基本から始めましょう



Macroidのインターフェースは、Brickと積み重ねられています。 ブリックは、マークアップまたは個別のウィジェットを表すインターフェイスブリックです。 レイアウトは「レイアウト」または単に「l」と表示され、ウィジェットは「ウィジェット」または「w」と表示されます。 たとえば、テキストボックスとボタンを持つ単純なLinearLayoutは次のようになります。



 l[LinearLayout]( w[TextView], w[Button] )
      
      





そのようなマークアップを変数に割り当て、そこで必要に応じて変数を互いに構成することを妨げるものは何もありません。



 val view = l[LinearLayout]( w[TextView], w[Button] )
      
      





もちろん、ウィジェットはいくつかのプロパティと値を規定するように設定する必要があります。そうしないと意味がありません。

これはTweakと呼ばれるものに役立ちます。 簡潔にするために、調整は演算子<〜で示されます。 たとえば、テキストをフィールドとボタンに設定できます。



 w[TextView] <~ text(" "), w[Button] <~ text(" ")
      
      





また、垂直調整を使用して、ウィジェットを垂直にする必要があることを正確に規定することもできます。



 val view = l[LinearLayout]( w[TextView] <~ text(" "), w[Button] <~ text(" ") ) <~ vertical
      
      





最後に、onClickListenerのないボタンは何ですか。 たとえば、ボタンをクリックするだけでフィールドのテキストを変更できます。



 w[Button] <~ text(" ") <~ On.click(changeText)
      
      





特別なマクロOnは、ドットの後に書き込まれたものからリスナー名を推測し、それが適用されているウィジェットで見つけようとします。 私たちの場合、彼はButtonウィジェットでonClickListenerを見つけ、そこにchangeText関数を登録しようとします。



テキストを変更するには、何らかの方法でフィールドウィジェットをchangeText関数に渡す必要があります。 スロットメソッドはこれに役立ちます。これにより、ウィジェットがOptionでラップされ、結果を変数に安全にバインドしてコードで使用できるようになります。



 var textView = slot[TextView]
      
      





特定のウィジェットは、ワイヤー調整を使用してスロットに接続されます。



 w[TextView] <~ wire(textView)
      
      





これでchangeTextメソッドの記述に戻ることができます。



 def changeText : Ui[Any] = { textView <~ text(" ") }
      
      





このメソッドは1つの調整で構成され、UIスレッドで実行されるアクションである同じUiアクションを返します。



結果のマークアップをMainActivityに適用するには、onCreateメソッドでsetContentView(getUi(view))を呼び出します。 getUi(view)メソッドは、現在のスレッドでUIコードを実行し、結果のビューを返します。これは、アクティビティのContentViewにインストールします。



結果のコードは、 Step2タグで使用できます。



異なる部分を同時に変更する



1つのUiアクション内で、異なるウィジェットに作用するいくつかの調整を組み合わせることができます。 この場合、微調整は次々に順番に開始されます。



しかし、複数のインターフェースの変更を同時に実行する必要がある場合はどうでしょうか? Snailと呼ばれる調整の代替手段がここで役立ち、演算子<~~によって示されます。 カタツムリは「ショットアンドフォーゲット」の原則に基づいて動作します。 たとえば、テキストフィールドに対してフェードアニメーションを実行できます。



 textView <~~ fadeOut(500)
      
      





複数のSnailを1つに連続して結合するには、++演算子を使用します。 これは、Snailが点滅するビューを探す方法です。



 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("  ")) }
      
      





ここで、最初のフィールドのボタンをクリックすると、テキストが変更されると同時に、マークアップの新しい部分が点滅し始め、アニメーションの終了後にテキストが再び変更されます。 これは、2つの演算子〜および~~を使用して実現されます。 最初のステートメントは両方のアクションを同時に起動し、2番目のステートメントは前のアクションが終了した後にのみ次のアクションを開始します。



結果のコードは、 Step3タグで使用できます。



リスト、リスト、リスト



確かに、アプリケーションを作成するときには、リストを表示する必要があります。 Macroidがこれをどのように処理するかを見てみましょう。

単純なリストを作成する場合は、Listableを使用できます。 Listable [A、W <:View]特性は、Wウィジェットを使用してタイプAのオブジェクトを正確に表示する方法を示します。これは、2つの簡単なステップで行われます。



  1. 空のビューを作成する
  2. データを入力します


空のTextViewを作成し、tweak text()を使用してテキストを入力し、Listable [String、TextView]型のオブジェクトを返すBasicListableメソッドを追加します。



 def basicListable(implicit appCtx: AppContext): Listable[String, TextView] = { Listable[String].tw { w[TextView] } { text(_) } }
      
      





ListViewをマークアップに追加し、Listable.listAdapterTweak調整を適用して、この単純なツイートを行からこの調整に渡すだけです。



 w[ListView] <~ basicListable.listAdapterTweak(contactList)
      
      





結果のコードは、 Step4タグで利用できます。



フラグメント



アプリケーションでフラグメントを使用したい場合、ここでMacroidが提供するものがあります。 まず、アクティビティを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」を使用してマークアップに挿入され、framedメソッドを使用してFrameLayoutにラップされます。



フラグメントにはIdとTagが必要であり、IdGenerationトレイトとIdGenおよびTagGenクラスを使用してそれらを作成することが提案されています。 TagとIdのポイントの後に目的のタグとidを書き留めて、framedメソッドに渡すだけで十分です。



 f[ListableFragment].framed(Id.contactList, Tag.contactList)
      
      





結果のコードはタグStep5で利用可能です



そして再びリスト



Androidでは、多数のウィジェットで構成される要素を含むリストをスクロールすると、アダプターがID(findViewById())による検索を頻繁に使用するため、パフォーマンスの問題が発生する場合があります。 この問題に対処する手段として、View Holderテンプレートが提案されています。



Macroidには、このテンプレートを実装するSlottedListable特性があり、複雑な要素を持つリストについては、ライブラリの作成者が使用することをお勧めします。 この特性を使用するには、1つのタイプと2つのメソッドを再定義する必要があります。



再定義するContactListableクラスを作成しましょう。



  1. クラスSlots。これはビューホルダーであり、必要なすべてのビューのスロットが含まれます。
  2. マークアップが作成されるmakeSlotsメソッド
  3. メソッドfillSlots。マークアップは渡された値で埋められます


ケースクラスの連絡先(名前:文字列、電話:文字列、写真:Int)を作成しましょう。名前、電話、写真を含む最小限の連絡先です。



fullContactListメソッドを作成して、3つの連絡先のリストを返します。



連絡先リストを本物のように見せるために、調整を使用して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フラグメントを作成するだけです。



1つの画面に2つの連絡先リストが少しあるので、古いListableFragmentを真新しい光沢のあるSlottedListableFragmentに置き換えることをお勧めします。 これを行うために、Macroidから親切に提供されたFragmentApiトレイトを使用して、1つのフラグメントを置き換える新しい調整を作成します。



 def replaceListableWithSlotted = Ui { activityManager(this).beginTransaction().replace(Id.contactList,new SlottedListableFragment,Tag.slottedList).commit() }
      
      





そして、この微調整を、長く苦しんでいるchangeTextメソッドの最後に追加します。このメソッドは、changeTextAndShowFragmentと呼ばれます。



結果のコードは、 Step6タグを使用して利用できます。



AkkaのないなんてScala



Scalaについて言えば、アクターとAkkaライブラリーを無視することはできません。



Macroidは、アクターを使用してフラグメント間でメッセージを転送することを提案し、これにAkkaFragment特性を提供します。 各AkkaFragmentは独自のアクターを作成します。 アクターは、アクティビティが存続している間、フラグメントは通常のライフサイクルで存続します。 したがって、アクターは、制御するインターフェースに参加したり、分離したりできます。



まず、build.sbtに依存関係macroid-akkaおよびakka-actorを追加する必要があります。 また、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 { *; }" )
      
      







ボタン1つで単純なフラグメントを作成してみましょう。ボタンのテキストの色は、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() }
      
      





その結果、テキストの色を変更するように互いに命令できるボタンを持つ2つのフラグメントを取得します。



結果のコードは、 Step7タグで使用できます。



おわりに



以上で私の記事は終わりです。 もちろん、マクロイドの一部の機能は舞台裏に残っていましたが、歯のためにこのライブラリを試してみたい、または少なくとも詳しく見てみたいという願望をなんとかしてもらいたいと思います。 このライブラリは現在、47 Degreesチームのメンバーによって開発されており、 Githubで入手できます。



ご清聴ありがとうございました。



All Articles