Java tutorial
/* * Copyright 2015 - 2017 Michael Rapp * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package de.mrapp.android.util.multithreading; import android.content.Context; import android.os.Handler; import android.os.Message; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.support.v4.util.LruCache; import android.view.View; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import de.mrapp.android.util.logging.LogLevel; import de.mrapp.android.util.logging.Logger; import static de.mrapp.android.util.Condition.ensureNotNull; /** * An abstract base class for all data binders, which allow to asynchronously load data in order to * display it by using views. Such binders are meant to be used, when loading various data items, of * which each one should be displayed by a different view. Once loaded, the data can optionally be * stored in a cache (it therefore must be associated with an unique key). When attempting to reload * already cached data, it is retrieved from the cache and displayed immediately. * * The binder supports to use adapter views, which might be recycled while data is still loaded. In * such case, the recycled view is prevented from showing the data once loading has finished, * because it is already used for other purposes. * * @param <DataType> * The type of the data, which is bound to views * @param <KeyType> * The type of the keys, which allow to uniquely identify already loaded data * @param <ViewType> * The type of the views, which are used to display data * @param <ParamType> * The type of parameters, which can be passed when loading data * @author Michael Rapp * @since 1.15.0 */ public abstract class AbstractDataBinder<DataType, KeyType, ViewType extends View, ParamType> extends Handler { /** * Defines the interface, a class, which should be notified about the progress of a {@link * AbstractDataBinder} must implement. * * @param <DataType> * The type of the data, which is bound to views * @param <KeyType> * The type of the keys, which allow to uniquely identify already loaded data * @param <ViewType> * The type of the views, which are used to display data * @param <ParamType> * The type of parameters, which can be passed when loading data */ public interface Listener<DataType, KeyType, ViewType extends View, ParamType> { /** * The method, which is invoked, when the data binder starts to load data asynchronously. * * @param dataBinder * The observed data binder as an instance of the class {@link AbstractDataBinder}. * The data binder may not be null * @param key * The key of the data, which should be loaded, as an instance of the generic type * KeyType. The key may not be null * @param params * An array, which contains optional parameters, as an array of the type ParamType * or an empty array, if no parameters should be used * @return True, if the loading the data should be proceeded, false otherwise. When * returning false, the method gets invoked repeatedly until true is returned. */ @SuppressWarnings("unchecked") boolean onLoadData(@NonNull AbstractDataBinder<DataType, KeyType, ViewType, ParamType> dataBinder, @NonNull KeyType key, @NonNull ParamType... params); /** * The method, which is invoked, when the data binder shows data, which has been loaded * either asynchronously or from cache. * * @param dataBinder * The observed data binder as an instance of the class {@link AbstractDataBinder}. * The data binder may not be null * @param key * The key of the data, which has be loaded, as an instance of the generic type * KeyType. The key may not be null * @param data * The data, which has been loaded, as an instance of the generic type DataType or * null, if no data has been loaded * @param view * The view, which is used to display the data, as an instance of the generic type * ViewType. The view may not be null * @param params * An array, which contains optional parameters, as an array of the type ParamType * or an empty array, if no parameters should be used */ @SuppressWarnings("unchecked") void onFinished(@NonNull AbstractDataBinder<DataType, KeyType, ViewType, ParamType> dataBinder, @NonNull final KeyType key, @Nullable DataType data, @NonNull final ViewType view, @NonNull final ParamType... params); /** * The method, which is invoked, when the data binder has been canceled. * * @param dataBinder * The observed data binder as an instance of the class {@link AbstractDataBinder}. * The data binder may not be null */ void onCanceled(@NonNull AbstractDataBinder<DataType, KeyType, ViewType, ParamType> dataBinder); } /** * A task, which encapsulates all information, which is required to asynchronously load data and * display it afterwards. It also contains the data once loaded. * * @param <DataType> * The type of the data, which is bound to views * @param <KeyType> * The type of the keys, which allow to uniquely identify already loaded data * @param <ViewType> * The type of the views, which are used to display data * @param <ParamType> * The type of parameters, which can be passed when loading data */ private static class Task<DataType, KeyType, ViewType extends View, ParamType> { /** * The view, which should be used to display the data. */ private final ViewType view; /** * The key of the data, which should be loaded. */ private final KeyType key; /** * An array, which contains optional parameters. */ private final ParamType[] params; /** * The data, which has been loaded. */ @Nullable private DataType result; /** * Creates a new task * * @param view * The view, which should be used to display the data, as an instance of the class * {@link View}. The view may not be null * @param key * The key of the data, which should be loaded, as an instance of the generic type * KeyType. The key may not be null * @param params * An array, which contains optional parameters, as an array of the type ParamType * or an empty array, if no parameters should be used */ Task(@NonNull final ViewType view, @NonNull final KeyType key, @NonNull final ParamType[] params) { this.view = view; this.key = key; this.params = params; this.result = null; } } /** * The number of items, which are stored by a cached, by default. */ public static final int CACHE_SIZE = 10; /** * The context, which is used by the data binder. */ private final Context context; /** * The logger, which is used by the data binder. */ private final Logger logger; /** * A set, which contains the listeners, which are notified about the progress of the data * binder. */ private final Set<Listener<DataType, KeyType, ViewType, ParamType>> listeners; /** * A LRU cache, which is used to cache already loaded data. */ private final LruCache<KeyType, DataType> cache; /** * A map, which is used to manage the views, which have already been used to display data. */ private final Map<ViewType, KeyType> views; /** * The thread pool, which is used to manage the threads, which are used to asynchronously load * data. */ private final ExecutorService threadPool; /** * The object, which is used to acquire locks, when cancelling to load data. */ private final Object cancelLock; /** * True, if loading the data has been canceled, false otherwise */ private boolean canceled; /** * True, if data should be cached, false otherwise. */ private boolean useCache; /** * Notifies all listeners, that the data binder starts to load data asynchronously. * * @param key * The key of the data, which should be loaded, as an instance of the generic type * KeyType. The key may not be null * @param params * An array, which contains optional parameters, as an array of the type ParamType or an * empty array, if no parameters should be used * @return True, if loading the data should be proceeded, false otherwise */ @SafeVarargs private final boolean notifyOnLoad(@NonNull final KeyType key, @NonNull final ParamType... params) { synchronized (listeners) { boolean result = true; for (Listener<DataType, KeyType, ViewType, ParamType> listener : listeners) { result &= listener.onLoadData(this, key, params); } return result; } } /** * Notifies all listeners, that the data binder is showing data, which has been loaded either * asynchronously or from cache. * * @param key * The key of the data, which has be loaded, as an instance of the generic type KeyType. * The key may not be null * @param data * The data, which has been loaded, as an instance of the generic type DataType or null, * if no data has been loaded * @param view * The view, which is used to display the data, as an instance of the generic type * ViewType. The view may not be null * @param params * An array, which contains optional parameters, as an array of the type ParamType or an * empty array, if no parameters should be used */ @SafeVarargs private final void notifyOnFinished(@NonNull final KeyType key, @Nullable final DataType data, @NonNull final ViewType view, @NonNull final ParamType... params) { synchronized (listeners) { for (Listener<DataType, KeyType, ViewType, ParamType> listener : listeners) { listener.onFinished(this, key, data, view, params); } } } /** * Notifies all listeners, that the data binder has been canceled. */ private void notifyOnCanceled() { synchronized (listeners) { for (Listener<DataType, KeyType, ViewType, ParamType> listener : listeners) { listener.onCanceled(this); } } } /** * Returns the data, which corresponds to a specific key, from the cache. * * @param key * The key of the data, which should be retrieved, as an instance of the generic type * KeyType. The key may not be null * @return The data, which has been retrieved, as an instance of the generic type DataType or * null, if no data with the given key is contained by the cache */ @Nullable private DataType getCachedData(@NonNull final KeyType key) { synchronized (cache) { return cache.get(key); } } /** * Adds the data, which corresponds to a specific key, to the cache, if caching is enabled. * * @param key * The key of the data, which should be added to the cache, as an instance of the * generic type KeyType. The key may not be null * @param data * The data, which should be added to the cache, as an instance of the generic type * DataType. The data may not be null */ private void cacheData(@NonNull final KeyType key, @NonNull final DataType data) { synchronized (cache) { if (useCache) { cache.put(key, data); } } } /** * Asynchronously executes a specific task in order to load data and to display it afterwards. * * @param task * The task, which should be executed, as an instance of the class {@link Task}. The * task may not be null */ private void loadDataAsynchronously(@NonNull final Task<DataType, KeyType, ViewType, ParamType> task) { threadPool.submit(new Runnable() { @Override public void run() { if (!isCanceled()) { while (!notifyOnLoad(task.key, task.params)) { try { Thread.sleep(100); } catch (InterruptedException e) { break; } } task.result = loadData(task); Message message = Message.obtain(); message.obj = task; sendMessage(message); } } }); } /** * Executes a specific task in order to load data. * * @param task * The task, which should be executed, as an instance of the class {@link Task}. The * task may not be null * @return The data, which has been loaded, as an instance of the generic type DataType or null, * if no data has been loaded */ @Nullable private DataType loadData(@NonNull final Task<DataType, KeyType, ViewType, ParamType> task) { try { DataType data = doInBackground(task.key, task.params); if (data != null) { cacheData(task.key, data); } logger.logInfo(getClass(), "Loaded data with key " + task.key); return data; } catch (Exception e) { logger.logError(getClass(), "An error occurred while loading data with key " + task.key, e); return null; } } /** * Sets, whether loading the data has been canceled, or not. * * @param canceled * True, if loading the data has been canceled, false otherwise */ private void setCanceled(final boolean canceled) { synchronized (cancelLock) { this.canceled = canceled; } } /** * The method, which is invoked on implementing subclasses prior to loading any data. This * method may be overridden to adapt the appearance of views. * * @param view * The view, which should be used to display the data, as an instance of the generic * type ViewType. The view may not be null * @param params * An array, which contains optional parameters, as an array of the type ParamType or an * empty array, if no parameters should be used */ @UiThread @SuppressWarnings("unchecked") protected void onPreExecute(@NonNull final ViewType view, @NonNull final ParamType... params) { } /** * The method, which is invoked on implementing subclasses, in order to load the data, which * corresponds to a specific key. This method is executed in a background thread and therefore * no views may be modified. * * @param key * The key of the data, which should be loaded, as an instance of the generic type * KeyType. The key may not be null * @param params * An array, which contains optional parameters, as an array of the type ParamType or an * empty array, if no parameters should be used * @return The data, which has been loaded, as an instance of the generic type DataType or null, * if no data has been loaded */ @Nullable @SuppressWarnings("unchecked") protected abstract DataType doInBackground(@NonNull final KeyType key, @NonNull final ParamType... params); /** * The method, which is invoked on implementing subclasses, in order to display data after it * has been loaded. * * @param view * The view, which should be used to display the data, as an instance of the generic * type ViewType. The view may not be null * @param data * The data, which should be displayed, as an instance of the generic type DataType or * null, if no data should be displayed * @param params * An array, which contains optional parameters, as an array of the type ParamType or an * empty array, if no parameters should be used */ @UiThread @SuppressWarnings("unchecked") protected abstract void onPostExecute(@NonNull final ViewType view, @Nullable final DataType data, @NonNull final ParamType... params); /** * Creates a new data binder. Caching is enabled by default. The cache, which is used to store * already loaded data, caches up to <code>CACHE_SIZE</code> items. The executor service, which * is used to manage asynchronous tasks, is created by using the static method * <code>Executors.newCachedThreadPool</code>. Such executor services are meant to be used when * many short-living tasks are executed and reuse previously created threads. * * @param context * The context, which should be used by the data binder, as an instance of the class * {@link Context}. The context may not be null */ public AbstractDataBinder(@NonNull final Context context) { this(context, Executors.newCachedThreadPool()); } /** * Creates a new data binder, which uses a specific executor service. Caching is enabled by * default. The cache, which is used to store already loaded data, caches up to * <code>CACHE_SIZE</code> items. * * @param context * The context, which should be used by the data binder, as an instance of the class * {@link Context}. The context may not be null * @param threadPool * The executor service, which should be used to manage asynchronous tasks, as an * instance of the type {@link ExecutorService}. The executor service may not be null */ public AbstractDataBinder(@NonNull final Context context, @NonNull final ExecutorService threadPool) { this(context, threadPool, new LruCache<KeyType, DataType>(CACHE_SIZE)); } /** * Creates a new data binder, which uses a specific cache. Caching is enabled by default. The * executor service, which is used to manage asynchronous tasks, is created by using the static * method <code>Executors.newCachedThreadPool</code>. Such executor services are meant to be * used when many short-living tasks are executed and reuse previously created threads. * * @param context * The context, which should be used by the data binder, as an instance of the class * {@link Context}. The context may not be null * @param cache * The LRU cache, which should be used to cache already loaded data, as an instance of * the class LruCache. The cache may not be null */ public AbstractDataBinder(@NonNull final Context context, @NonNull final LruCache<KeyType, DataType> cache) { this(context, Executors.newCachedThreadPool(), cache); } /** * Creates a new data binder, which uses a specifc executor service and cache. Caching is * enabled by default. * * @param context * The context, which should be used by the data binder, as an instance of the class * {@link Context}. The context may not be null * @param threadPool * The executor service, which should be used to manage asynchronous tasks, as an * instance of the type {@link ExecutorService}. The executor service may not be null * @param cache * The LRU cache, which should be used to cache already loaded data, as an instance of * the class LruCache. The cache may not be null */ public AbstractDataBinder(@NonNull final Context context, @NonNull final ExecutorService threadPool, @NonNull final LruCache<KeyType, DataType> cache) { ensureNotNull(context, "The context may not be null"); ensureNotNull(threadPool, "The executor service may not be null"); ensureNotNull(cache, "The cache may not be null"); this.context = context; this.logger = new Logger(LogLevel.INFO); this.listeners = new LinkedHashSet<>(); this.cache = cache; this.views = Collections.synchronizedMap(new WeakHashMap<ViewType, KeyType>()); this.threadPool = threadPool; this.cancelLock = new Object(); this.canceled = false; this.useCache = true; } /** * Returns the context, which is used by the data binder. * * @return The context, which is used by the data binder, as an instance of the class {@link * Context}. The context may not be null */ @NonNull public final Context getContext() { return context; } /** * Returns the log level, which is used for logging. * * @return The log level, which is used for logging, as a value of the enum {@link LogLevel}. * The log level may not be null */ @NonNull public final LogLevel getLogLevel() { return logger.getLogLevel(); } /** * Sets the log level, which should be used for logging. * * @param logLevel * The log level, which should be set, as a value of the enum {@link LogLevel}. The log * level may not be null */ public final void setLogLevel(@NonNull final LogLevel logLevel) { logger.setLogLevel(logLevel); } /** * Adds a new listener, which should be notified about the events of the data binder. * * @param listener * The listener, which should be added, as an instance of the type {@link Listener}. The * listener may not be null */ public final void addListener(@NonNull final Listener<DataType, KeyType, ViewType, ParamType> listener) { ensureNotNull(listener, "The listener may not be null"); synchronized (listeners) { listeners.add(listener); } } /** * Removes a specific listener, which should not be notified about the events of the data * binder, anymore. * * @param listener * The listener, which should be removed, as an instance of the type {@link Listener}. * The listener may not be null */ public final void removeListener(@NonNull final Listener<DataType, KeyType, ViewType, ParamType> listener) { ensureNotNull(listener, "The listener may not be null"); synchronized (listeners) { listeners.remove(listener); } } /** * Loads the the data, which corresponds to a specific key, and displays it in a specific view. * If the data has already been loaded, it will be retrieved from the cache. By default, the * data is loaded in a background thread. * * @param key * The key of the data, which should be loaded, as an instance of the generic type * KeyType. The key may not be null * @param view * The view, which should be used to display the data, as an instance of the generic * type ViewType. The view may not be null * @param params * An array, which contains optional parameters, as an array of the type ParamType or an * empty array, if no parameters should be used */ @SafeVarargs public final void load(@NonNull final KeyType key, @NonNull final ViewType view, @NonNull final ParamType... params) { load(key, view, true, params); } /** * Loads the the data, which corresponds to a specific key, and displays it in a specific view. * If the data has already been loaded, it will be retrieved from the cache. * * @param key * The key of the data, which should be loaded, as an instance of the generic type * KeyType. The key may not be null * @param view * The view, which should be used to display the data, as an instance of the generic * type ViewType. The view may not be null * @param async * True, if the data should be loaded in a background thread, false otherwise * @param params * An array, which contains optional parameters, as an array of the type ParamType or an * empty array, if no parameters should be used */ @SafeVarargs public final void load(@NonNull final KeyType key, @NonNull final ViewType view, final boolean async, @NonNull final ParamType... params) { ensureNotNull(key, "The key may not be null"); ensureNotNull(view, "The view may not be null"); ensureNotNull(params, "The array may not be null"); setCanceled(false); views.put(view, key); DataType data = getCachedData(key); if (!isCanceled()) { if (data != null) { onPostExecute(view, data, params); notifyOnFinished(key, data, view, params); logger.logInfo(getClass(), "Loaded data with key " + key + " from cache"); } else { onPreExecute(view, params); Task<DataType, KeyType, ViewType, ParamType> task = new Task<>(view, key, params); if (async) { loadDataAsynchronously(task); } else { data = loadData(task); onPostExecute(view, data, params); notifyOnFinished(key, data, view, params); } } } } /** * Cancels loading the data. */ public final void cancel() { setCanceled(true); notifyOnCanceled(); logger.logInfo(getClass(), "Canceled to load data"); } /** * Returns, whether loading the data has been canceled, or not. * * @return True, if loading the data has been canceled, false otherwise */ public final boolean isCanceled() { synchronized (cancelLock) { return canceled; } } /** * Returns, whether the data, which corresponds to a specific key, is currently cached, or not. * * @param key * The key, which corresponds to the data, which should be checked, as an instance of * the generic type KeyType. The key may not be null * @return True, if the data, which corresponds to the given key, is currently cached, false * otherwise */ public final boolean isCached(@NonNull final KeyType key) { ensureNotNull(key, "The key may not be null"); synchronized (cache) { return cache.get(key) != null; } } /** * Returns, whether data is cached, or not. * * @return True, if data is cached, false otherwise */ public final boolean isCacheUsed() { synchronized (cache) { return useCache; } } /** * Sets, whether data should be cached, or not. * * @param useCache * True, if data should be cached, false otherwise. */ public final void useCache(final boolean useCache) { synchronized (cache) { this.useCache = useCache; logger.logDebug(getClass(), useCache ? "Enabled" : "Disabled" + " caching"); if (!useCache) { clearCache(); } } } /** * Clears the cache. */ public final void clearCache() { synchronized (cache) { cache.evictAll(); logger.logDebug(getClass(), "Cleared cache"); } } @SuppressWarnings("unchecked") @Override public final void handleMessage(final Message msg) { Task<DataType, KeyType, ViewType, ParamType> task = (Task) msg.obj; if (!isCanceled()) { KeyType key = views.get(task.view); if (key != null && key.equals(task.key)) { onPostExecute(task.view, task.result, task.params); notifyOnFinished(task.key, task.result, task.view, task.params); } else { logger.logVerbose(getClass(), "Data with key " + task.key + " not displayed. View has been recycled"); } } else { logger.logVerbose(getClass(), "Data with key " + task.key + " not displayed. Loading data has been canceled"); } } }