Автоматизация разработки под Android

в 8:24, , рубрики: android development, code generation, Разработка под android, метки: ,
Автоматизация разработки под Android

Приветствую, хаброжители. Я Android разработчик. Точнее даже не так. Я ленивый Android разработчик. На протяжении уже не одного десятка проектов меня начали угнетать рутинные и механические операции, не требующие от меня никаких умственных усилий. Теперь же за меня это делает IDE.

Введение

На данный момент есть несколько способов для облегчения жизни android разработчику. Первым для меня был RoboJuice с его аннотациями для инициализации View. На первый взгляд все хорошо, читабельность не сильно портится. Проблемы начались, когда мне стало необходимо наследовать Activity от какой-то сторонней для получения необходимой функциональности. Но так как все Activity наследуются от RoboActivity, это превратилось в набор костылей. После этого к подобным библиотекам у меня выработался здравый скептицизм. К этой же группе можно отнести AndroidAnnotations, ButterKnife и пр. Все они либо генерируют поверх кода много своего, либо делают совсем тривиальные вещи. Например, в ButterKnife:

@InjectView(R.id.title) TextView title;

вместо

title = (TextView) findViewById(R.id.title);

При использовании автозаполнения и quickfix это займет абсолютно столько же времени. А если нет разницы — зачем платить больше добавлять еще одну библиотеку в проект.

Второй — это использование шаблонов IDE (http://habrahabr.ru/post/183502/ ) Лично не пробовал, но скорее всего требует выработки некоторой привычки.

Другие решения

На протяжении последнего месяца меня терзал один вопрос — почему никто до сих пор не сделал скрипт/плагин/%свой вариант% для генерации кода из xml layout. Первым делом я решил погуглить на эту тему. Единственное, что я нашел, было http://code.google.com/p/android-code-generator-plugin/. Простой плагин для Eclipse, который парсит xml и делает на его основе %name%Activity.java. Также у разработчика есть статья в блоге. Все остальное было либо генерация в браузере, либо вопросы «А где можно взять?».

Запустив его пару раз, я нашел ряд вещей, которые меня не устраивали:
— инициализация view непосредственно в onCreate();
— пропиcывание пакета для генерации вручную в общих настройках;
— не выставляются кастомные шрифты для всех View. По умолчанию это Roboto, для того чтобы приложение выглядело одинаково на всех версиях Android.
— реализация Listeners на уровне Activity. Когда их общее количество превышает 3-4 — код читать становится очень трудно;
— генерация всех View, у которых есть id. Не вижу смысла, так как какой-нибудь контейнер(например, LinearLayout) редко будет использоваться в коде.
— нет генерации простого ArrayAdapter с описанным Holder. Вещь довольно простая, но занимает время;
— судя по коммитам, проект очень медленно развивается.

После тщетных попыток поискать еще, понял — надо писать самому.

В результате недолгих размышлений и общения с коллегами было составлено ТЗ:
— автоматически генерировать пакеты на основе манифеста проекта, если они не существуют;
— генерировать код только для тех View, которым можно установить шрифт или обработку действия;
— выделить 3 метода для инициализации view, шрифтов, listeners. Если все это писать вместе в onCreate в больших проектах, получится стена кода;
— если в xml встречается ListView, сделать диалог с предложением выбрать xml-файл с версткой элемента листа для генерации адаптера.

В качестве основы был взят выше описанный проект, так как он уже содержит некоторые готовые решения. Например, данный метод выбирает из xml все View, у которых был прописан id.


private NodeList getNodesWithId(InputStream inputStream) throws ParserConfigurationException, SAXException,
 IOException, XPathExpressionException {
 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
 // factory.setNamespaceAware(true); // never forget this!
 DocumentBuilder builder = factory.newDocumentBuilder();
 Document doc = builder.parse(inputStream);
 XPathFactory pathFactory = XPathFactory.newInstance();
 XPath xPath = pathFactory.newXPath();
 XPathExpression expression = xPath.compile("//*[@id]");
 return (NodeList) expression.evaluate(doc, XPathConstants.NODESET);
}

Далее были созданы три класса ViewGenerator, ActivityGenerator, AdapterGenerator.
В каждом из этих классов содержатся строковые шаблоны для различных кусков кода, характерные для каждого.
Например:


private final static String FIELD_PATTERN = "tprivate %1$s %2$s;n";
private final static String METHOD_VOID_PATTERN = "tprivate void %1$s(){n%2$st}n";	

ViewGenerator генерирует весь код, связанный с инициализацией View из xml(объявление полей, findViewById, setTypeFace, setListener и все необходимые импорты). Причем как для Activity, так и для ViewHolder адаптера. Например, генерация для setListeners:


	private String getListeners(boolean innerClass){
		StringBuilder builder = new StringBuilder();
		for (WidgetResource widgetResource : widgetsTypes.keySet()) {
			if (BUTTON_WIDGETS.contains(widgetsTypes.get(widgetResource).getName())) {
				builder.append(String.format(innerClass ? ONCLICK_INNER_PATTERN : ONCLICK_PATTERN, widgetResource.getVariableName()));
			}
		}
		return builder.toString();
	}

ActivityGenerator генерирует код Activity c использованием генерации ViewGenerator:


  public String generate() {
		StringBuilder stringBuilder = new StringBuilder();
		if (packageName != null && !packageName.isEmpty()) {
			stringBuilder.append(getPackage());
			stringBuilder.append("n");
		}
		stringBuilder.append(getImports());
		stringBuilder.append(viewGenerator.getImports());
		stringBuilder.append("n");
		
		StringBuilder innerBuilder = new StringBuilder();
		innerBuilder.append(getTag());
		innerBuilder.append("n");
		innerBuilder.append(viewGenerator.getFields(false));
		innerBuilder.append("n");
		innerBuilder.append(getCreateMethod());
		innerBuilder.append("n");
		innerBuilder.append(getInitActionBarMethod());
		innerBuilder.append("n");
		innerBuilder.append(viewGenerator.getInitViewsMethod(false));
		innerBuilder.append("n");
		innerBuilder.append(viewGenerator.getSetFontsMethod(false));
		innerBuilder.append("n");
		innerBuilder.append(viewGenerator.getSetListenersMethod(false));
		stringBuilder.append(String.format(HEADER_PATTERN, activityResource.getVariableName(), innerBuilder.toString()));
		return stringBuilder.toString();
	}

AdapterGenerator соответственно генерирует код Adapter:


	public String generate() {
		StringBuilder stringBuilder = new StringBuilder();
		if (packageName != null && !packageName.isEmpty()) {
			stringBuilder.append(getPackage());
			stringBuilder.append("n");
		}
		stringBuilder.append(getImports());
		stringBuilder.append("n");
		stringBuilder.append(getHeader());
		stringBuilder.append("n");
		stringBuilder.append(getTag());
		stringBuilder.append("n");
		stringBuilder.append(FIELDS_PATTERN);
		stringBuilder.append("n");
		stringBuilder.append(getConstructor());
		stringBuilder.append("n");
		stringBuilder.append(getGetView());
		stringBuilder.append("n");
		stringBuilder.append(getHolder());
		stringBuilder.append("}");
		stringBuilder.append("n");
		return stringBuilder.toString();
	}

И, наконец, пример работы:

Activity

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <ListView
        android:id="@+id/news_list_activity_news_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <ImageView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"

        <ProgressBar
            android:id="@+id/articles_progress"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <TextView
            android:id="@+id/articles_exist_text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
             />

    <Button
            android:id="@+id/articles_exist_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </RelativeLayout>

</RelativeLayout> 

Элемент листа

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:paddingLeft="15dp"
    android:paddingRight="15dp" >

    <TextView
        android:id="@+id/row_news_date_header_text"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
         />

    <RelativeLayout
        android:id="@+id/news_main_layout"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" >

        <FrameLayout
            android:id="@+id/row_news_image_container"
            android:layout_width="96dp"
            android:layout_height="68dp"
             >

            <ImageView
                android:id="@+id/row_news_icon"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="fitXY" />
        </FrameLayout>

        <TextView
            android:id="@+id/row_news_title_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            />

        <TextView
            android:id="@+id/row_news_type_text"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            />

        <ImageView
            android:id="@+id/row_news_date_icon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            />

        <TextView
            android:id="@+id/row_news_date_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            />
    </RelativeLayout>

    <View
        android:id="@+id/row_news_items_divider"
        android:layout_width="fill_parent"
        android:layout_height="1dp"
        android:background="@drawable/divider_gray_horizontal" />

</LinearLayout>

Результат


package com.test.activity;

import android.os.Bundle;
import android.app.Activity;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.graphics.Typeface;
import com.test.R;
import android.widget.ListView;
import android.widget.Button;
import android.view.View.OnClickListener;
import android.view.View;

public class NewsListActivityActivity extends Activity {
    private static final String TAG = NewsListActivityActivity.class.getSimpleName();

    private ProgressBar articlesProgress;
    private ListView newsListActivityNewsList;
    private TextView articlesExistTextView;
    private Button articlesExistButton;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.news_list_activity);

        initActionBar();
        initViews();
        setFonts();
        setListeners();
    }

    private void initActionBar(){
    }

    private void initViews(){
        articlesProgress = (ProgressBar) findViewById(R.id.articles_progress);
        newsListActivityNewsList = (ListView) findViewById(R.id.news_list_activity_news_list);
        articlesExistTextView = (TextView) findViewById(R.id.articles_exist_text_view);
        articlesExistButton = (Button) findViewById(R.id.articles_exist_button);
    }

    private void setFonts(){
        Typeface roboto = null;//TODO init this by utils
        articlesExistTextView.setTypeface(roboto);
    }

    private void setListeners(){
        articlesExistButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        });
    }
}


package com.test.adapter;

import com.test.R;
import android.graphics.Typeface;
import android.content.Context;
import java.util.List;
import android.widget.ArrayAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.graphics.Typeface;
import android.widget.ImageView;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import android.view.View.OnClickListener;
import android.view.View;

public class NewsListActivityAdapter extends ArrayAdapter<String>{
    private static final String TAG = NewsListActivityAdapter.class.getSimpleName();

    private Context context;
    private LayoutInflater inflater;


    public NewsListActivityAdapter(Context context, List<String> objects) {
        super(context, R.layout.news_list_item, objects);
        inflater = LayoutInflater.from(context);
        this.context = context;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null){
            convertView = inflater.inflate(R.layout.news_list_item, parent, false);
            holder = new ViewHolder(convertView);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        String item = getItem(position);
        if (item != null){
            holder.populateForm(item);      }
        return convertView;
    }
    private class ViewHolder{

        private TextView rowNewsDateHeaderText;
        private TextView rowNewsDateText;
        private TextView rowNewsTypeText;
        private TextView rowNewsTitleText;

        public ViewHolder(View v){
            initViews(v);
            setFonts();
        }
        private void initViews(View v){
            rowNewsDateHeaderText = (TextView) v.findViewById(R.id.row_news_date_header_text);
            rowNewsDateText = (TextView) v.findViewById(R.id.row_news_date_text);
            rowNewsTypeText = (TextView) v.findViewById(R.id.row_news_type_text);
            rowNewsTitleText = (TextView) v.findViewById(R.id.row_news_title_text);
        }

        private void setFonts(){
            Typeface roboto = null;//TODO init this by utils
            rowNewsDateHeaderText.setTypeface(roboto);
            rowNewsDateText.setTypeface(roboto);
            rowNewsTypeText.setTypeface(roboto);
            rowNewsTitleText.setTypeface(roboto);
        }

        public void populateForm(String item) {

        }
    }
}

Как пользоваться

1. Копируем jar файл плагина в папку plugins Eclipse.
2. Перезапускаем Eclipse.
3. Выделяем необходимые xml-файлы сверстанных экранов.
4. Правой кнопкой на выбранных файлах -> Android Code Generator.
5. Используем полученные файлы из пакетов %packagename%.activity и %packagename%.adapter.

Заключение

Я надеюсь, что это кому-нибудь сделает жизнь легче. Плагин работает на Eclipse 4.2. Все исходники можно найти тут.

Спасибо за внимание.

P.S. Для начала решил сделать плагин для Eclipse. В будущем думаю сделать такой же для Android Studio.

Автор: 3JIaD9Ipa

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js