すべてのプログラマは定型コードに直面しています。 特にAndroidプログラマー。 定型コードの作成はありがたい仕事であり、それを楽しんでいるプログラマーはいないと確信しています。 ある晴れた日、私は解決策を探し始めました。 アイデアは非常に単純であるという事実にもかかわらず、別のクラスでテンプレートコードを生成し、後で実行時にこのコードを呼び出しますが、既製のソリューションはなく、私は仕事に取り掛かりました。 最初のバージョンは、作業中のプロジェクトのサブモジュールの1つとして実装されました。 2年以上、この決定に満足しています。 本当に期待通りに機能しました。 時間が経つと、モジュールに新しい機能が追加され、リファクタリングされ、最適化されました。 一般に、 PoCは成功と呼ばれる可能性があるため、プロジェクトをコミュニティと共有することにしました。
夕方に8か月のプログラミングをした後、私は人生で最初の投稿でHabréにいます。 したがって、 Jetaはjavax.annotation.processing
上に構築されたソースコードを生成するためのフレームワークです。 オープンソース 、 Apache 2.0 、GitHubのソースコード、jCenterのアーティファクト、チュートリアル、サンプル、単体テスト、一般的に、すべてが正常に機能しています。
わかりやすくするために、簡単な例を見てみましょう。 ライブラリには@Log
アノテーションが含まれています。 これを使用すると、クラス内の名前付きロガーの宣言が簡単になります。
public class LogSample { @Log Logger logger; }
そのため、このクラスのLogSample_Metacode
、 applyLogger
はapplyLogger
メソッドをLogSample_Metacode
クラスを生成しLogSample_Metacode
。
public class LogSample_Metacode implements LogMetacode<LogSample> { @Override public void applyLogger(LogSample master, NamedLoggerProvider provider) { master.logger = (Logger) provider.getLogger(“LogSample”); } }
この例は、 @Log
アノテーションを使用して、「LogSample」という名前のロガーをアノテーション付きフィールドに割り当てるコードが生成されることを示しています。 プロジェクトで使用されるライブラリからロガーを提供するNamedLoggerProvider
を実装することは残っています。
例からわかるように、クラス名から取得されるロガーの暗黙の命名に加えて、 @Log(“REST”)
などの注釈パラメーターを使用して特定の値を指定できます。
この手法により、次のような文字列のコピー&ペーストが不要になります。
private final Logger logger = LoggerFactory.getLogger(LogSample.class);
多くの場合、プログラマーはパラメーターとして渡されたクラスを置き換えることを忘れるので、「隣人」の名前を持つロガーからプロジェクトを保存します。
もちろん、これは非常に単純な例です。 ただし、フレームワークの主なアイデアを示しています-コードを少なくし、安定性を高めます。
Jetaの主な目標はテンプレートコードを取り除くことですが、上記の受付では、 Dependency Injection 、 Event Bus 、 Validatorsなど、多くの便利な機能が実装されています。これらはすべてフレームワークの原則に従って書かれていることに注意してください。 Java Reflectionと、可能であれば、すべてのエラーはコンパイル段階にあります。
この記事では、架空のボイラープレートのケースも取り除きません。 代わりに、有用なもの、つまりデータバインディング (以降DB)を作成します。 ただし、基本的な違いはありません。この記事は、テンプレートコードの削除に関連する問題を解決するためのガイドとして使用できます。
データバインディング。
Androidプログラマーはすでにこの用語に精通しているかもしれません。 少し前、GoogleはData Binding Libraryをリリースしました。 このパターンに精通していない人にとっては、この記事の例からその概念に対処することは難しくないと確信しています。 また、それぞれAndroidとData-Bindingで短いエクスカーションを持つ2つのネタバレを用意しています。
Androidプログラミングのコンテキストでは、画面はActivityと呼ばれます。 これはandroid.app.Activity
から継承されたJavaクラスです。 各アクティビティには、 レイアウトと呼ばれるマークアップ付きのXMLファイルがあります 。 「Hello、World」アプリケーションのアクティビティの例を次に示します。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/text1" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView text1 = (TextView) findViewById(R.id.text1); text1.setText("Hello World!"); } }
setContentView(R.layout.activity_main)
行setContentView(R.layout.activity_main)
は、自動生成されるR
ファイルを介してアクティビティとレイアウトを接続します。 したがって、レイアウトactivity_main.xml
場合、Rファイルにはactivity_mainフィールドと一意の数値を持つ内部レイアウトクラスが含まれます。 id = text1
を割り当てたTextViewの場合、これはそれぞれ内部クラスid
とフィールドtext1
になります。
データバインディングを使用すると、 XMLファイル内にDSL式を記述できます。 developer.android.comの公式Webサイトからの例を次に示します。
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="user" type="com.example.User"/> </data> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName}"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.lastName}"/> </LinearLayout> </layout>
そのため、適切なタイミングで、ユーザーオブジェクト( com.example.User
)をレイアウトにバインドし、データバインディングが対応するコンポーネントに値を自動的に配置します。 したがって、最初のTextView
はユーザー名TextView
表示され、2番目のTextView
姓TextView
表示されます。
この記事では、これまでのところ、データバインディングを優先的に記述しますが、最終的には少しインタラクティブになります。
始める前に、Jetaについていくつかコメントします。
Android固有のすべての機能は、個別のライブラリーAndrojetaに移動されます。 これはJetaを拡張します。つまり、Jetaで利用可能なすべてのもの、つまり Androjetaでも利用可能なJavaプロジェクト用。
- フレームワークの用語では、生成されたクラスはMetacodeと呼ばれます 。 メタコードが生成されるクラスはMasterと呼ばれます。 ウィザードにメタコードを適用するコントローラーもあり、 Metasitoryはすべてのメタコードクラスへのリンクのリポジトリです。 Metasitoryを使用して、コントローラーは適切なメタコードを見つけます。
1. DataBinding
プロジェクト
まず、1つのアクティビティとUser pojoクラスを使用して、最も一般的なAndroidプロジェクトを作成します。 ここでのタスクは、記事の最後にあるDBを使用して、対応するUIコンポーネントにユーザーの姓と名を書き込むことです。 明確にするために、プロジェクトの構造を示すスクリーンショットを提供します。
2. common
モジュール
コード生成はコンパイル段階で行われ、それに付随するすべてのクラスは個別の環境で実行されるため、実行時とコード生成中の両方で使用できるモジュールが必要です。 これは、 DataBind
アノテーションとDataBind
メタコードインターフェイスの2つのファイルを含む通常のJavaモジュールであることに注意してください。
3. apt
モジュール
aptモジュールには、コード生成に必要なクラスが含まれています。 すでに述べたように、このモジュールはcommonに依存しており、コンパイル段階でのみ利用可能になります。 一般的なように 、これは単一のファイルDataBindProcessor
を含む通常のJavaモジュールです。 このクラスでは、 DataBind
アノテーションを処理し、 XMLレイアウトを解析し、対応するメタコードを生成します。 aptモジュールはorg.brooth.androjeta:androjeta-apt:+:noapt
にも依存しているため、フレームワーククラスにアクセスできることに注意してください。
4. app
を準備する
メタコードの生成に直接進む前に、まずアプリケーションを準備する必要があります。 まず、レイアウトを変更します。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androjeta="http://schemas.jeta.brooth.org/androjeta" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/firstName" androjeta:setText="master.user.firstName" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:id="@+id/lastName" androjeta:setText="master.user.lastName" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
少し明確化:プレフィックス "androjeta"を使用して名前空間を宣言し、 androjeta:setText
2つのandrojeta:setText
属性を追加しましたandrojeta:setText
DB式を使用したandrojeta:setText
。 したがって、 DataBindProcessor
でこれらの式を見つけて処理し、適切なDataBindProcessor
を生成できます。
package org.brooth.androjeta.samples.databinding; import android.app.Activity; import android.os.Bundle; @DataBind(layout = "activity_main") public class MainActivity extends Activity { final User user = new User("John", "Smith"); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MetaHelper.applyDataBinding(this); } }
ここでは2つのことが重要です。 最初に、以前に共通モジュールで作成した@DataBind
に@DataBind
アノテーションを追加しました。 したがって、生成段階で、Jetaはこのクラスを見つけてDataBindProcessor
渡します。 次に、レイアウトを設定した後、 MetaHelper.applyDataBind(this)
を呼び出します。 これらの静的メソッドを使用すると、メタコードに簡単にアクセスできます。 このクラスを作成しましょう。
package org.brooth.androjeta.samples.databinding; import org.brooth.jeta.metasitory.MapMetasitory; import org.brooth.jeta.metasitory.Metasitory; public class MetaHelper { private static MetaHelper instance = new MetaHelper("org.brooth.androjeta.samples"); private final Metasitory metasitory; private MetaHelper(String metaPackage) { metasitory = new MapMetasitory(metaPackage); } public static void applyDataBinding(Object master) { new DataBindController<>(instance.metasitory, master).apply(); } }
MetaHelperはオプションのクラスです。 これは、メタコードの呼び出しを整理する方法です。 便宜上のみです。 このページでこのクラスの詳細を読むことができます 。 ここで重要なのは、 applyDataBinding
メソッドがDataBindController
作業を以下に渡すDataBindController
です。
package org.brooth.androjeta.samples.databinding; import org.brooth.jeta.MasterController; import org.brooth.jeta.metasitory.Metasitory; public class DataBindController<M> extends MasterController<M, DataBindMetacode<M>> { public DataBindController(Metasitory metasitory, M master) { super(metasitory, master, DataBind.class); } public void apply() { for(DataBindMetacode<M> metacode : metacodes) metacode.apply(master); } }
思い出してください、コントローラーはメタコードをマスターに適用するクラスです。 詳細については、 このページをご覧ください 。
最後の手順では、 DataBindProcessor
を生成するためにJetaが呼び出すプロセッサのリストにDataBindProcessor
を追加する必要があります。 これを行うには、appモジュールのルートパッケージ( app/src/main/java
)で、コンテンツを含むファイルjeta.properties
を作成します。
processors.add = org.brooth.androjeta.samples.databinding.apt.DataBindProcessor metasitory.package = org.brooth.androjeta.samples application.package = org.brooth.androjeta.samples.databinding
このファイルと利用可能な設定の詳細については、 このページをご覧ください 。
5. DataBindProcessor
プロセッサのすべてのステップについてコメントする必要はないと思います。なぜなら、 革新的なものは含まれていません。 主なポイントを説明するだけで十分です。SAXパーサーでXMLレイアウトを調べ、 DB式を見つけ、対応するJavaコードを生成します。
Jetaは 、Javaコードを生成するためにSquareの すばらしいライブラリであるJavaPoetを使用していることに注意してください 。 独自のプロセッサを作成する予定がある場合は、 READMEを読むことをお勧めします。 以下はDataBindProcessor
のソースコードです。
package org.brooth.androjeta.samples.databinding.apt; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeSpec; import org.brooth.androjeta.samples.databinding.DataBind; import org.brooth.androjeta.samples.databinding.DataBindMetacode; import org.brooth.jeta.apt.ProcessingContext; import org.brooth.jeta.apt.ProcessingException; import org.brooth.jeta.apt.RoundContext; import org.brooth.jeta.apt.processors.AbstractProcessor; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import java.io.File; import java.io.FileNotFoundException; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; public class DataBindProcessor extends AbstractProcessor { private static final String XMLNS_PREFIX = "xmlns:"; private static final String ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android"; private static final String ANDROJETA_NAMESPACE = "http://schemas.jeta.brooth.org/androjeta"; private ClassName textViewClassname; private ClassName rCLassName; private String layoutsPath; private String androidPrefix; private String androjetaPrefix; private String componentId; private String componentExpression; public DataBindProcessor() { super(DataBind.class); } @Override public void init(ProcessingContext processingContext) { super.init(processingContext); layoutsPath = processingContext.processingEnv().getOptions().get("layoutsPath"); if (layoutsPath == null) throw new ProcessingException("'layoutsPath' not defined"); String appPackage = processingContext.processingProperties().getProperty("application.package"); if (appPackage == null) throw new ProcessingException("'application.package' not defined"); textViewClassname = ClassName.bestGuess("android.widget.TextView"); rCLassName = ClassName.bestGuess(appPackage + ".R"); } @Override public boolean process(TypeSpec.Builder builder, final RoundContext roundContext) { TypeElement element = roundContext.metacodeContext().masterElement(); ClassName masterClassName = ClassName.get(element); builder.addSuperinterface(ParameterizedTypeName.get( ClassName.get(DataBindMetacode.class), masterClassName)); final MethodSpec.Builder methodBuilder = MethodSpec. methodBuilder("apply") .addAnnotation(Override.class) .addModifiers(Modifier.PUBLIC) .returns(void.class) .addParameter(masterClassName, "master"); String layoutName = element.getAnnotation(DataBind.class).layout(); String layoutPath = layoutsPath + File.separator + layoutName + ".xml"; File layoutFile = new File(layoutPath); if (!layoutFile.exists()) throw new ProcessingException(new FileNotFoundException(layoutPath)); androidPrefix = null; androjetaPrefix = null; try { SAXParserFactory factory = SAXParserFactory.newInstance(); SAXParser saxParser = factory.newSAXParser(); saxParser.parse(layoutFile, new DefaultHandler() { @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { for (int i = 0; i < attributes.getLength(); i++) { if (androidPrefix == null && attributes.getQName(i).startsWith(XMLNS_PREFIX) && attributes.getValue(i).equals(ANDROID_NAMESPACE)) { androidPrefix = attributes.getQName(i).substring(XMLNS_PREFIX.length()); continue; } if (androjetaPrefix == null && attributes.getQName(i).startsWith(XMLNS_PREFIX) && attributes.getValue(i).equals(ANDROJETA_NAMESPACE)) { androjetaPrefix = attributes.getQName(i).substring(XMLNS_PREFIX.length()); continue; } if (componentId == null && androidPrefix != null && attributes.getQName(i).equals(androidPrefix + ":id")) { componentId = attributes.getValue(i).substring("@+id/".length()); continue; } if (componentExpression == null && androjetaPrefix != null && attributes.getQName(i).equals(androjetaPrefix + ":setText")) { componentExpression = attributes.getValue(i); } } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if (componentExpression == null) return; if (componentId == null) throw new ProcessingException("Failed to process expression '" + componentExpression + "', component has no id"); methodBuilder.addStatement("(($T) master.findViewById($T.id.$L))\n\t.setText($L)", textViewClassname, rCLassName, componentId, componentExpression); componentId = null; componentExpression = null; } } ); } catch (Exception e) { throw new ProcessingException(e); } builder.addMethod(methodBuilder.build()); return false; } }
6.使用する
まず、すべてが機能することを確認します。 これを行うには、プロジェクトディレクトリで次のコマンドを実行します。
./gradlew assemble
出力にエラーがなく、エントリが表示される場合:
Note: Metacode built in Xms
その後、すべてがOKで、パス/app/build/generated/source/apt/
に沿って、生成されたコードを見ることができます:
ご覧のとおり、メタコードはフォーマットされており、読みやすいため、デバッグが容易です。 また、重要なプラスは、すべての可能なエラーがコンパイル段階で検出されることです。 そのため、 user
フィールドのないアクティビティに@DataBind
を追加したり、パラメーターに間違ったレイアウト名を渡したり、 DB式を間違えたりすると、生成されたコードはコンパイルされず、プロジェクトはアセンブルされません。
この時点で、アプリケーションを起動でき、予想どおり、画面にユーザーユーザーデータが表示されます。
7.結論。
既成のソリューションとしてではなく、 Proof-Of-Conceptとして例を参照してください。 さらに、彼の仕事は、 Jeta-DBが稼働するという事実ではなく、フレームワークの動作を実証することです。
実際、約束されたインタラクティブ。 Data-Bindingに表示する内容をコメントに記入します。 おそらく、Googleからのいくつかの実装の機会を逃しています。 おそらく、他の定型的なケースを取り除きたいと思うでしょう。 また、他のコメントや提案にも感謝します。 次に、最も興味深いものを選択し、将来のバージョンで実装しようとします。
最後まで読んでくれてありがとう。
幸せなコード生成! :)
» 公式サイト
» GitHubサンプルソースコード
» GitHub上のJeta
» GitHubのAndrojeta