Pull to refresh
70
0
Sergey Solovyev @serso

User

Send message
Надо, Активити может будет пересоздана по многим другим причинам, например, split-screen или если пользователь просто сменил дату-время-язык на телефоне.
Многие такие велосипеды писали, сейчас нужно смотреть в сторону ViewModel/LiveData и их интеграций (например, с Room).
Убогая кодогенерация и плохая поддержка генерации ключей в таблице.
По поводу кодогенерации: если у вас есть какой-нибудь объект, который вы хотите записать в базу, то после добавления аннотаций GreenDAO модифицирует класс добавляя ненужные геттеры/сеттеры. Об иммутабельности класса можно забыть.
Далее, поле id должно быть обязательно объявлено @Nullable для того, чтобы GreenDAO смог обновить его после вставки в базу, т.е. о примитивных типах можно забыть.

В последнем моём проекте попробовал GreenDAO и меня ждало разочарование. Переписал всё вручную в итоге. Возможно, есть и более удачные фреймворки

Не увидел вашего вопроса вовремя. Purchase flow необходим только при совершении покупки, для загрузки инвентаря он не нужен. В одной из последних версий либы для удобства отладки был добавлен Logger, который можно выставить через Billing#setLogger.
Не могу ничего сказать про серверную часть, но на клиенте можно было бы использовать какое-нибудь готовое решение, например, библиотеку Checkout.
Это, похоже, единственный способ совершить покупку из виджета. Если вы не хотите, чтобы пользователь переходил на дополнительный экран, можете сделать активити прозрачной
Нет, о библиотечной зависимости:
compile ('org.solovyev.android:checkout:x.x.x@aar') {
    transitive = true
}

Всё дело в том, что если в Gradle у зависимости указать классификатор (classifier, aar в данном случае), то свойство «transitive» будет выставлено в false, т.е. зависимости зависимостей не подтянутся автоматически.
Можно было просто добавить transitive = true для зависимости
Нет, но это не отменяет принципов Material Design :)
Несколько явных проблем:
  • шрифты (сравните, например, с карточками в моём словаре)
  • иконки (а точнее кругляши вокруг иконок, например, «копировать», «добавить в избранное» и т.д.)
  • странные элементы управления в разделе Разговорник — кнопки с непонятными иконками и другими декоративными элементами
Вот, пожалуйста: Duktig
Использует math.js и умеет всё, что умеет math.js. Дополнительно можно писать комментарии (//) и заголовки (# и ##).
Для удобства ввода использовал кнопки из Калькулятора++, т.е. для ввода символа нужно тянуть кнопку вверх, вниз или вбок.
Пользуйтесь на здоровье.
Да, там просто подсчёт ссылок идёт.
Если вы добавили 3 фрагмента в back stack, то вам нужно будет нажать 3 раза кнопку back, чтобы их оттуда убрать. Четвёртое нажатие закроет activity, т.к. back stack будет пустым. Чтобы правильно решить проблему создавайте первый фрагмент без добавления в back stack.
В MyLinearLayoutManager есть ряд проблем, которые я решил при написании одного из своих приложений Say it right!. Я решил поделиться наработкой с миром и теперь LinearLayoutManager с поддержкой WRAP_CONTENT доступен в виде библиотеки. Подробности на странице проекта.
Код класса:
package org.solovyev.android.views.llm;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;

/**
 * {@link android.support.v7.widget.LinearLayoutManager} which wraps its content. Note that this class will always
 * wrap the content regardless of {@link android.support.v7.widget.RecyclerView} layout parameters.
 *
 * Now it's impossible to run add/remove animations with child views which have arbitrary dimensions (height for
 * VERTICAL orientation and width for HORIZONTAL). However if child views have fixed dimensions
 * {@link #setChildSize(int)} method might be used to let the layout manager know how big they are going to be.
 * If animations are not used at all then a normal measuring procedure will run and child views will be measured during
 * the measure pass.
 */
public class LinearLayoutManager extends android.support.v7.widget.LinearLayoutManager {

	private static final int CHILD_WIDTH = 0;
	private static final int CHILD_HEIGHT = 1;
	private static final int DEFAULT_CHILD_SIZE = 100;

	private final int[] childDimensions = new int[2];

	private int childSize = DEFAULT_CHILD_SIZE;
	private boolean hasChildSize;

	@SuppressWarnings("UnusedDeclaration")
	public LinearLayoutManager(Context context) {
		super(context);
	}

	@SuppressWarnings("UnusedDeclaration")
	public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
		super(context, orientation, reverseLayout);
	}

	public static int makeUnspecifiedSpec() {
		return View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
	}

	@Override
	public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
		final int widthMode = View.MeasureSpec.getMode(widthSpec);
		final int heightMode = View.MeasureSpec.getMode(heightSpec);

		final int widthSize = View.MeasureSpec.getSize(widthSpec);
		final int heightSize = View.MeasureSpec.getSize(heightSpec);

		final boolean exactWidth = widthMode == View.MeasureSpec.EXACTLY;
		final boolean exactHeight = heightMode == View.MeasureSpec.EXACTLY;

		final int unspecified = makeUnspecifiedSpec();

		if (exactWidth && exactHeight) {
			// in case of exact calculations for both dimensions let's use default "onMeasure" implementation
			super.onMeasure(recycler, state, widthSpec, heightSpec);
			return;
		}

		final boolean vertical = getOrientation() == VERTICAL;

		initChildDimensions(widthSize, heightSize, vertical);

		int width = 0;
		int height = 0;

		// it's possible to get scrap views in recycler which are bound to old (invalid) adapter entities. This
		// happens because their invalidation happens after "onMeasure" method. As a workaround let's clear the
		// recycler now (it should not cause any performance issues while scrolling as "onMeasure" is never
		// called whiles scrolling)
		recycler.clear();

		final int stateItemCount = state.getItemCount();
		final int adapterItemCount = getItemCount();
		// adapter always contains actual data while state might contain old data (f.e. data before the animation is
		// done). As we want to measure the view with actual data we must use data from the adapter and not from  the
		// state
		for (int i = 0; i < adapterItemCount; i++) {
			if (vertical) {
				if (!hasChildSize) {
					if (i < stateItemCount) {
						// we should not exceed state count, otherwise we'll get IndexOutOfBoundsException. For such items
						// we will use previously calculated dimensions
						measureChild(recycler, i, widthSpec, unspecified, childDimensions);
					} else {
						logMeasureWarning(i);
					}
				}
				height += childDimensions[CHILD_HEIGHT];
				if (i == 0) {
					width = childDimensions[CHILD_WIDTH];
				}
				if (height >= heightSize) {
					break;
				}
			} else {
				if (!hasChildSize) {
					if (i < stateItemCount) {
						// we should not exceed state count, otherwise we'll get IndexOutOfBoundsException. For such items
						// we will use previously calculated dimensions
						measureChild(recycler, i, unspecified, heightSpec, childDimensions);
					} else {
						logMeasureWarning(i);
					}
				}
				width += childDimensions[CHILD_WIDTH];
				if (i == 0) {
					height = childDimensions[CHILD_HEIGHT];
				}
				if (width >= widthSize) {
					break;
				}
			}
		}

		if ((vertical && height < heightSize) || (!vertical && width < widthSize)) {
			// we really should wrap the contents of the view, let's do it

			if (exactWidth) {
				width = widthSize;
			} else {
				width += getPaddingLeft() + getPaddingRight();
			}

			if (exactHeight) {
				height = heightSize;
			} else {
				height += getPaddingTop() + getPaddingBottom();
			}

			setMeasuredDimension(width, height);
		} else {
			// if calculated height/width exceeds requested height/width let's use default "onMeasure" implementation
			super.onMeasure(recycler, state, widthSpec, heightSpec);
		}
	}

	private void logMeasureWarning(int child) {
		if (BuildConfig.DEBUG) {
			Log.w("LinearLayoutManager", "Can't measure child #" + child + ", previously used dimensions will be reused." +
					"To remove this message either use #setChildSize() method or don't run RecyclerView animations");
		}
	}

	private void initChildDimensions(int width, int height, boolean vertical) {
		if (childDimensions[CHILD_WIDTH] != 0 || childDimensions[CHILD_HEIGHT] != 0) {
			// already initialized, skipping
			return;
		}
		if (vertical) {
			childDimensions[CHILD_WIDTH] = width;
			childDimensions[CHILD_HEIGHT] = childSize;
		} else {
			childDimensions[CHILD_WIDTH] = childSize;
			childDimensions[CHILD_HEIGHT] = height;
		}
	}

	@Override
	public void setOrientation(int orientation) {
		// might be called before the constructor of this class is called
		//noinspection ConstantConditions
		if (childDimensions != null) {
			if (getOrientation() != orientation) {
				childDimensions[CHILD_WIDTH] = 0;
				childDimensions[CHILD_HEIGHT] = 0;
			}
		}
		super.setOrientation(orientation);
	}

	public void clearChildSize() {
		hasChildSize = false;
		setChildSize(DEFAULT_CHILD_SIZE);
	}

	public void setChildSize(int childSize) {
		hasChildSize = true;
		if (this.childSize != childSize) {
			this.childSize = childSize;
			requestLayout();
		}
	}

	private void measureChild(RecyclerView.Recycler recycler, int position, int widthSpec, int heightSpec, int[] dimensions) {
		final View child = recycler.getViewForPosition(position);

		final RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) child.getLayoutParams();

		final int hPadding = getPaddingLeft() + getPaddingRight();
		final int vPadding = getPaddingTop() + getPaddingBottom();

		final int hMargin = p.leftMargin + p.rightMargin;
		final int vMargin = p.topMargin + p.bottomMargin;

		final int hDecoration = getRightDecorationWidth(child) + getLeftDecorationWidth(child);
		final int vDecoration = getTopDecorationHeight(child) + getBottomDecorationHeight(child);

		final int childWidthSpec = getChildMeasureSpec(widthSpec, hPadding + hMargin + hDecoration, p.width, canScrollHorizontally());
		final int childHeightSpec = getChildMeasureSpec(heightSpec, vPadding + vMargin + vDecoration, p.height, canScrollVertically());

		child.measure(childWidthSpec, childHeightSpec);

		dimensions[CHILD_WIDTH] = getDecoratedMeasuredWidth(child) + p.leftMargin + p.rightMargin;
		dimensions[CHILD_HEIGHT] = getDecoratedMeasuredHeight(child) + p.bottomMargin + p.topMargin;

		recycler.recycleView(child);
	}
}

Если в настройках приложения выставить user-client десктопный проблема остаётся? Вполне может быть, что Tomato отдаёт разный контент для разных устройств.
Да, проглядел код этой библиотеки перед написанием своей. Есть ряд проблем, например:
1. BillingProcessor работает только с Activity. Т.е. загрузить данные о покупках, например, в Application или Service просто невозможно.
2. Каждый раз при старте Activity происходит переподключение биллинг сервиса, что не эффективно, т.к. Activity убивается каждый раз при повороте экрана (у меня он подключается один раз при старте приложения).
3. BillingProcessor может вызвать методы слушателя тогда, когда это не нужно. Самый простой пример — нужно загрузить список продуктов или купить продукт в Activity. Если пользователь повернёт экран, то Activity убивается, но слушатель, который был создан остаётся. При вызове методов последнего скорее всего приложение навернётся, т.к. Activity уже разрушено.
4. consumePurchase и другие методы следует вызывать на отдельном потоке (см. доки). В этой библиотеки они вызываются на основном потоке, что может привести к неотзывчивому интерфейсу.
5. Нет юнит тестов. Для библиотеки по работе с деньгами считаю это неприемлемым.

В целом код невысокого качества — это заметно даже после беглого осмотра (форматирование, доки, название переменных и т.д.)
Я с вами согласен — помогать проектам надо, свои велосипеды обычно не нужны. Но в данном конкретном случае слишком много различий, например, абсолютно разный API. Вы же не хотите, чтобы у всех кто используют старый API поломались сборки после обновления?

Про поддержку проекта — библиотека работает уже сейчас, расширяться особо нечему. Нужно только будет фиксить баги, но в этом я заинтересован сам, так как использую библиотеку в своих приложениях.
Что за стандартная Google Service библиотека? Код из семплов?

Вот список того, что библиотека берёт на себя:
1. Асинхронное подключение IInAppBillingService. Если на момент выполнения биллинг запроса сервис не подключён — запрос встаёт в очередь и ждёт подключения.
2. Многопоточность — некоторые запросы должны выполняться на отдельном потоке, например, getSkuDetails или consumePurchase (см. документацию).
3. Проверка на поддержку биллинга.
4. Типовые задачи — парсинг json, обработка ошибок, действия нужные для покупки и т.д.
5. Отмена запросов (и слушателей). Полезно, например, при закрытии Activity.
6. Возможность выполнять запросы не из Activity.
7. Кеширование результатов.
8. Код Activity становится проще за счёт того, что код биллинга находится в отдельных классах.
Не в обиду будет сказано, но:
1. Это не эффектиный и даже неправильно работающий код — новый поток создаётся каждый раз, `flagEndAsync` находится не в finally блоке, доступ к `setupState` не синхронизован:
      (new Thread(new Runnable() {
            public void run() {
                final List<IabResult> results = new ArrayList<IabResult>();
                for (Purchase purchase : purchases) {
                    try {
                        consume(purchase);
                        results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
                    } catch (IabException ex) {
                        results.add(ex.getResult());
                    }
                }

                flagEndAsync();
                if (setupState != SETUP_DISPOSED && singleListener != null) {
                    notifyHandler.post(new Runnable() {
                        public void run() {
                            singleListener.onConsumeFinished(purchases.get(0), results.get(0));
                        }
                    });
                }
                if (setupState != SETUP_DISPOSED && multiListener != null) {
                    notifyHandler.post(new Runnable() {
                        public void run() {
                            multiListener.onConsumeMultiFinished(purchases, results);
                        }
                    });
                }
            }
        })).start();

2. Судя по всему, OpenIAB не работает в многопоточном окружении (Billing из моей библиотеки thread safe)
3. Использование переменных `mAsyncInProgress` и `mAsyncOperation` без синхронизации, вы серьёзно?
4. Где тесты?
И это только при беглом осмотре одного класса, который, кстати, несколько великоват для его нормальной поддержки (~2k строк кода).

Подводя итог, я бы не стал использовать OpenAIB в своих проектах, так как качество кода оставляет желать лучшего.
1
23 ...

Information

Rating
Does not participate
Location
Санкт-Петербург, Санкт-Петербург и область, Россия
Date of birth
Registered
Activity