пятница, 21 декабря 2012 г.

"Тяп-ляп и готово"?

На сайте startandroid.ru Дмитрий довольно интересно рассказывает как делать приложения для Android. Я постепенно продвигаюсь по урокам и сейчас меня заинтересовал вот этот: Урок 18. Меняем layoutParams в рабочем приложении

В этом уроке делается простое приложение:


(картинку я взял из этого урока, описание как на своем смартфоне сделать такую же лежит в нем же).
Двигаем ползунок - ширина кнопок меняется. Очень просто.
Вот такой код предлагает автор:



public class MainActivity extends Activity implements OnSeekBarChangeListener {

 
SeekBar sbWeight;
  Button btn1;
  Button btn2;

  LinearLayout.LayoutParams lParams1;
  LinearLayout.LayoutParams lParams2;

 
/** Called when the activity is first created. */
 
@Override
 
public void onCreate(Bundle savedInstanceState) {
   
super.onCreate(savedInstanceState);
    setContentView
(R.layout.main);

    sbWeight =
(SeekBar) findViewById(R.id.sbWeight);
    sbWeight.setOnSeekBarChangeListener
(this);

    btn1 =
(Button) findViewById(R.id.btn1);
    btn2 =
(Button) findViewById(R.id.btn2);

    lParams1 =
(LinearLayout.LayoutParams) btn1.getLayoutParams();
    lParams2 =
(LinearLayout.LayoutParams) btn2.getLayoutParams();
 
}

 
@Override
 
public void onProgressChanged(SeekBar seekBar, int progress,
     
boolean fromUser) {
   
int leftValue = progress;
   
int rightValue = seekBar.getMax() - progress;
   
// настраиваем вес
   
lParams1.weight = leftValue;
    lParams2.weight = rightValue;
   
// в текст кнопок пишем значения переменных
   
btn1.setText(String.valueOf(leftValue));
    btn2.setText
(String.valueOf(rightValue));
 
}


Это фрагмент,  с полным кодом можно знакомиться тут: Урок 18. Меняем layoutParams в рабочем приложении

<dis>Тут мне следует сделать небольшое отступление от темы и сразу договориться с читателем: программировать под Android учит Дмитрий. У него отличная серия уроков. В этом посте вы не научитесь тому чему учит Дмитрий. Мое изучение урока 18 мне же самому напомнило анекдот Эволюция пpогpаммиста и мне захотелось об этом написать.
</dis>

В своей работе я не допускаю появление такого кода. Здесь явно смешаны объекты представляющие контролы, дополнительный код который управляет этими контролами, и данные которые показывают эти контролы.  В примере заложена такая архитектура, которая ведет к проблемам в перспективе примерно больше чем полгода, если вероятны изменения в этом коде.

Вот что меня в нем не устроило:

1. sbWeight.setOnSeekBarChangeListener(this);
Здесь происходит смешивание функционала на уровне метода. Очень вероятное расширение поведения - это добавление второго SeekBar. Понятно что для него должна быть другая обработка. При использовании одного метода нужно делать дополнительное перенаправление логики в методе onProgressChanged (который один на всех) на специфичную для контрола логику (которая у каждого своя). Метод распухнет, появится неинтересный для чтения код. Поэтому мне сразу хочется убрать this и декларировать отдельный самостоятельный объект.

2. lParams1.weight = leftValue;
Здесь тоже происходит смешивание функционала. тут вероятны два развития приложения: новый контрол который тоже регулирует баланс веса (например Stars) и новый контрол который тоже отображает веса. В обоих случаях будет происходить добавление кода в этот метод и он станет решать сразу несколько задач. Метод тоже распухнет с неинтересным кодом.

3. public void onProgressChanged(
Здесь написана вся логика по реагированию на действия пользователя над контролами (действия когда от контрола приходит нотификация) и по управлению весами кнопок. Очень вероятное изменение: реализация этого или очень похожего функционала в новой форме (Copy&Paste?).

3.                                            (да-да, самому смешно, но это именно "дырка", или "отсутствие кода")
Ненадежное хранение значения: в приведенной архитектуре значение веса хранится в контроле SeekBar и копируется кодом в два других контрола (кнопки). Если добавится новый контрол для управления весом, то возникнет неоднозначность: у которого из них брать  значение? Ведь понятно что логически значение единственно и даже вопроса такого возникать не должно. Здесь не хватает отдельного (единственного) свойства для хранения этого значения (а контролы могут к нему обращаться что бы скопировать из него значение для собственного использования и логически это будет копия).

Я попробовал сделать аналогичное приложение и добавил новые контролы: кнопки и progress динамически показывают значение весов, Seek и Rating позволяют изменить вес и оба этих контрола тоже динамически показывают изменения (поменял вес в одном - другой отреагировал). Вот так это выглядит:



Вот такой получился код инициализации у формы:

 WeightBalance weightBalance;
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    weightBalance = new WeightBalance();
    weightBalance.setWeightBalance(50);
    weightBalance.setWeightBalanceMax(100);

    new Buttons_WieghtVisualizer(weightBalance,
        (Button)this.findViewById(R.id.btnLeft),
        (Button)this.findViewById(R.id.btnRight),
        this.findViewById(R.id.buttonsParent));

    new ProgressBar_WeightBalanceVisualizer((IWeightBalance)weightBalance,
        (ProgressBar)this.findViewById(R.id.progressBar1));

    new SeekBar_WeightBalanceController(weightBalance
        (SeekBar)this.findViewById(R.id.seekBar1));

    new RatingBar_WeightBalanceController(weightBalance
        (RatingBar)this.findViewById(R.id.ratingBar1));
}

Источник данных (а это два значения: WeightBalance и WeightBalanceMax) реализован как отдельный объект (класс WeightBalance) с двумя свойствами . В эти свойства можно записать значение, а с помощью эвентов можно динамично реагировать на изменения значений в этих свойствах.

Вот такая реализация источника данных:

public class WeightBalance implements IWeightBalance, IEditableWeightBalance {
    private int weightBalance;
    private int weightBalanceMax;
    private List<IOnWeightBalanceChangeListener> onWeightBalanceChangeListeners;

    public WeightBalance() {
        onWeightBalanceChangeListeners = new ArrayList<IOnWeightBalanceChangeListener>();
    }
    @Override
    public int getWeightBalance() {
        return weightBalance;
    }
    @Override
    public int getWeightBalanceMax() {
        return weightBalanceMax;
    }
    public void setWeightBalanceMax(int max) {
        this.weightBalanceMax = max;
    }
    @Override
    public void setWeightBalance(int newValue) {
        if(newValue > weightBalanceMax) {
            //TODOthrow new Exception("WeightBalance cannot be more than WeightBalanceMax");
        }
        this.weightBalance = newValue;
        for(IOnWeightBalanceChangeListener listener : onWeightBalanceChangeListeners) {
            listener.OnWeightBalanceChanged(weightBalance);
        }
    }
    public void addOnWeightBalanceChangeListener(IOnWeightBalanceChangeListener listener) {
        if(!onWeightBalanceChangeListeners.contains(listener)) {
            onWeightBalanceChangeListeners.add(listener);
        }
    }
    public void removeOnWeightBalanceChangeListener(IOnWeightBalanceChangeListener listener) {
        onWeightBalanceChangeListeners.remove(listener);
    }

Вот класс привязки значения свойства WeightBalance к двум кнопкам:

public class Buttons_WieghtVisualizer implements IOnWeightBalanceChangeListener {
    Button leftButton;
    Button rightButton;
    View refreshView;
    IWeightBalance weightBalance;
    public Buttons_WieghtVisualizer(IWeightBalance weightBalance, Button leftButton, Button rightButton, View refreshView) {
        this.weightBalance = weightBalance;
        this.weightBalance.addOnWeightBalanceChangeListener(this);
        this.leftButton = leftButton;
        this.rightButton = rightButton;
        this.refreshView = refreshView;
        OnWeightBalanceChanged(this.weightBalance.getWeightBalance());
    }
    @Override
    public void OnWeightBalanceChanged(int newValue) {
        LinearLayout.LayoutParams leftLP = 
            (LinearLayout.LayoutParams)this.leftButton.getLayoutParams();
        leftLP.weight = newValue;

        LinearLayout.LayoutParams rightLP = (LinearLayout.LayoutParams)this.rightButton.getLayoutParams();
rightLP.weight = weightBalance.getWeightBalanceMax() - newValue;
        this.refreshView.requestLayout();
    }
}

Этому классу требуется знать значение и получать нотификации об изменении этого значения что бы динамически применять его на свойства кнопок. Этот класс реализует передачу WeightBalance в одном направлении: из источника данных в контрол.

Вот такой один класс привязки значения свойства WeightBalance к двум кнопкам:

public class SeekBar_WeightBalanceController implements OnSeekBarChangeListener, IOnWeightBalanceChangeListener {
    private IEditableWeightBalance weightBalance;
    private SeekBar seekBar;
    public SeekBar_WeightBalanceController(IEditableWeightBalance weightBalance, SeekBar seekBar) {
        this.weightBalance = weightBalance;
        this.weightBalance.addOnWeightBalanceChangeListener(this);
        this.seekBar = seekBar;
        this.seekBar.setMax(weightBalance.getWeightBalanceMax());
        this.seekBar.setProgress(weightBalance.getWeightBalance());
        this.seekBar.setOnSeekBarChangeListener(this);
    }
    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        this.weightBalance.removeOnWeightBalanceChangeListener(this);
weightBalance.setWeightBalance(progress);
        this.weightBalance.addOnWeightBalanceChangeListener(this);
    }
    @Override
    public void OnWeightBalanceChanged(int newValue) {
        this.seekBar.setOnSeekBarChangeListener(null);
        this.seekBar.setProgress(newValue);
        this.seekBar.setOnSeekBarChangeListener(this);
    }
}

В этом классе реализована уже двухсторонняя передача значения: из свойств контрола в свойство WeightBalance и из свойства WeightBalance  в свойства контрола.

Остальные классы и интерфейсы можно посмотреть в исходниках: Example_SeekBar.Sources

Вот собственно и все. Получился банальный двунаправленный (а местами и вовсе в одну сторону) биндинг, уже давно реализованный в WPF. Вот уж где мне надо было бы просто указать имена свойств что бы стандартный класс биндинга понял из какого свойства в объекте брать значение и в какое свойство в контроле его присваивать (+еще эвент от меня нужен в моем источнике данных что бы динамически сонхронизировать их).

Еще раз окинув взглядом мою реализацию, которая конечно же очень правильная (кто бы сомневался :-D ) я считаю материал самого урока вполне подходящим: "просто и доступно про Android".

И все эти мои выкрутасы с перестановкой кода ориентированы на другую цель ("продолжает работать, работать и работать") и основаны на других задачах (ресурсов на поддержку нет, а отвечать на вопросы и исправлять ошибки надо). Мой код меняется, поэтому он должен быть очень устойчив к вносимым изменениям. Мой код читается гораздо больше раз чем пишется, поэтому он должен быть понятен в крупных и мелких деталях. Архитектура моего приложения известна только автору (которого обычно уже нет под рукой), поэтому код должен четко и кратко заявлять как он устроен, что делает и где в нем место новому коду даже человеку который в первый раз его видит (обычное дело в нашей работе).

Исходники тут: Example_SeekBar.Sources
Готовый apk тут: Example_SeekBar.apk