Kitek's Page Blog programisty / webdeveloper'a

Async.js

Programując w języku JavaScript prędzej czy później natkniemy się na asynchroniczne callback’i, które w dużych ilościach mogą tworzyć swojego rodzaju “wodospady”. Poniższy kod w dużym uproszczeniu obrazuje omawiany przypadek:

function doSomething(id, callback) {
	var cacheName = "someCache-"+id;
	// Check cache first
	getFromCache(cacheName, function(results) {
		if(null === results) {
			// if null get from database
			getFromDb("SELECT * FROM t WHERE id = "+id,function(rows) {
				// set cache 
				setToCache(cacheName, rows, function(err) {
					// return values
					callback(rows);
				});
			});
		} else {
			// return cached values
			callback(results);
		}
	});
}

Przedstawiony kod jest po pierwsze mało czytelny, po drugie posiada wiele zagnieżdżeń, a po trzecie można łatwo nadpisac wyniki poprzednich funkcji. Powyższy kod w łatwy sposób możemy zrefaktoryzować przy pomocy biblioteki Async.js.

Async.js jest biblioteką początkowo zaprojetktowaną dla Node.js ,jednak działa również w nowoczesnych przeglądarkach. Oferuje szereg ciekawych możliwości, około 20 funkcji usprawniających m.in. tworzenie asynchronicznych bloków kodu. Poniżej zostały opisane tylko wybrane z nich:

.series(tasks, [callback])

Uruchamia funkcje w podanej kolejności. Po zakończeniu jednej funkcji uruchamiana jest następna. Jeżeli któryś z podanych bloków zwróci błąd do callback’a przerywane jest wykonywanie całej kolejki. W przypadku poprawnego wykonania wszystkich kroków wyniki zwracane są jako tablica.

function doSomethingOther(params, results) {
	async.series([
		function checkIfUserExists(callback) {
			// do something
			callback(null, 'exists');
		},
		function checkIfUserIsBanned(callback) {
			// do something
			if(isBanned) {
				// fail, break queue
				callback('User is banned');
				return;
			}
			callback(null, 'notBanned');
		},
		function checkIfUserHasPrivileges(callback) {
			// do something
			callback(null, 'hasprivileges');
		}
	], 
	function result(err, results){
		if(err) {
			// something went wrong
		} else {
			// results = ['exists','notBanned','hasprivileges'];
		}
	});
}

Pierwszym parametrem funkcji result jest err, który zawiera informację o ewentualnym błędzie. Domyślna wartość (w przypadku braku błedu) err to undefined, warto o tym pamiętać tworząc porównania.

.waterfall(tasks, [callback])

Funkcja podoba do wcześniej omówionej series z tą różnicą, że dodatkowo umożliwia przekazywanie wyników do następnego kroku.

async.waterfall([
	function step1(callback) {
		callback(null, 'result 1');
	},
	function step2(arg1, callback) {
		// arg1 = 'result 1'
		callback(null, 'result 2', 'result 3');
	},
	function step3(arg1, arg2, callback) {
		callback(null, 'result 4');
	}],
	function result(err, lastResult) {
		// lastResult = 'result 4'
	}
);

W tym przypadku domyślna wartość err to null.

.parallel(tasks, [callback])

async.parallel([
	function step1(callback) {
		setTimeout(function() {
			console.log('step1');
			callback(null, 'result 1');	
		}, 8000);
	},
	function step2(callback) {
		setTimeout(function() {
			//callback(null, 'result 2');
			console.log('step2');
			callback('Error!');
		}, 50);
	},
	function step3(callback) {
		setTimeout(function() {
			console.log('step3');
			callback(null, 'result 3');
		}, 100);
	}],
	function result(err, results) {
		console.log(err);
		console.log(results);
	}
);

Umożliwia uruchomienie kilku funkcji jednocześnie. W przypadku poprawnego wykonania się każdej funkcji wyniki zwracane są w odpowiednio posortowanej tablicy results. Gdy wystąpi bład to główny callback wywoływany jest natychmiast. Uruchomione wcześniej funkcje nie zostaną przerwane. Domyślna wartość (w przypadku braku błedu) err to undefined.

Android Universal Image Loader

Gdy rozpoczynałem przygodę z platformą Android nie spodziewałem się, że pobranie i wyświetlenie obrazka będzie w jakiś sposób bardziej skomplikowane. Okazało się, że bez użycia dodatkowych bibliotek nie jest to sztuka łatwa (jak dla początkującej osoby). Z pomocą przyszła biblioteka Android-Universal-Image-Loader, którą z powodzeniem wykorzystuje w kilku projektach. Poniższy artykuł dotyczy wersji v1.9.2.

Instalacja

Najprostszym sposobem jest sciągnięcie biblioteki w postaci pliku jar i umieszczenie go w katalogu libs naszego projektu. Wymagane również będzie zdefiniowanie dodakowych uprawnień w pliku AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Zalecene jest umieszczenie konfiguracji ImageLoader’a w klasie Application w ten sposób będziemy mieli pewność, że biblioteka zostanie skonfigurowana tylko raz i dostępna będzie w całym naszym projekcie. Do tego niezbędne będzie umieszczenie atrybutu android:name="pl.kitek.MojaAplikacja" w manifeście:

<application android:name="pl.kitek.MojaAplikacja">...</application>

Oczywiście musimy stworzyć tak podaną klasę i umieścić ją we wskazanym pakiecie:

import android.app.Application;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;

public class MojaAplikacja extends Application {

	@Override
	public void onCreate() {
		super.onCreate();
		
		// Domyślna konfiguracja ImageLoader'a
		ImageLoaderConfiguration config = new ImageLoaderConfiguration
			.Builder(getApplicationContext())
			.build();

		ImageLoader.getInstance().init(config);
	}
}

Od tego momentu biblioteka jest gotowa do użycia. Będzie działać na domyślnych ustawieniach, które możemy modyfikować.

Wyświetlanie obrazków

Dopuszczalne jest stosowanie następujących typów adresów:

String url = "http://kitek.pl/public/uploads/2014/05/flux.jpg";
String url = "file:///mnt/sdcard/obrazek.png";
String url = "content://media/external/audio/albumart/13";
String url = "assets://obrazek.png";

Dostępnych jest kilka sposobów załadowania i wyświetlenia obrazka. Sposób pierwszy:

/res/layout/activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <ImageView
        android:id="@+id/obrazek"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:contentDescription="@string/app_name" />

</RelativeLayout>

MainActivity.java

public class MainActivity extends Activity {
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		String url = "http://kitek.pl/public/uploads/2014/05/flux.jpg";
		ImageView obrazek = (ImageView) findViewById(R.id.obrazek);
		
		ImageLoader.getInstance().displayImage(url, obrazek);
	}
}

Drugi sposób wykorzystuje SimpleImageLoadingListener i wymaga własnej obłsugi Bitmap:

ImageLoader.getInstance().loadImage(url, new SimpleImageLoadingListener() {
	@Override
	public void onLoadingStarted(String uri, View view) {
		Log.d("Loader", "Loading Started");
	}

	@Override
	public void onLoadingComplete(String uri, View view, Bitmap loadedImage) {
		Log.d("Loader", "Loading Complete");
		if (null != loadedImage && null != obrazek) {
			obrazek.setImageBitmap(loadedImage);
		}
	}

	@Override
	public void onLoadingCancelled(String uri, View view) {
		Log.d("Loader", "Loading Cancelled");
	}

	@Override
	public void onLoadingFailed(String uri, View view, FailReason failReason) {
		Log.d("Loader", "Loading Failed");
	}
});

Konfiguracja

Biblioteka posiada bardzo szerokie możliwości konfiguracyjne. Ustawienia podzielone zostały na dwie kategorie: DisplayImageOptions i ImageLoaderConfiguration. Konfiguracja biblioteki powinna zostać zrealizowana w klasie Application. Poniżej przedstawiono tylko wybrane opcje (większość dostępna jest na stronie autora).

DisplayImageOptions displayOptions = new DisplayImageOptions.Builder()
	// Umożliwia wyświetlenie "zaślepek" na czas: 
	// ładowania, braku Uri lub błędu załadowania docelowego obrazka
	.showImageOnLoading(R.drawable.ic_stub)
	.showImageForEmptyUri(R.drawable.ic_empty)
	.showImageOnFail(R.drawable.ic_error)

	// Zezwala na korzystanie z buforów
	.cacheInMemory(true)
	.cacheOnDisc(true)

	.build();
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
	// Określa kolejność ładowania kolejnych obrazków
	.tasksProcessingOrder(QueueProcessingType.FIFO)
	// Blokuje ładowanie do pamięci tego samego obrazka w kilku wymiarach
	.denyCacheImageMultipleSizesInMemory()
	
	// Strategia buforowania obrazków w pamięci.
	.memoryCache(new LruMemoryCache(2 * 1024 * 1024))
	// Maksymalny rozmiar bufora (2 MB)
	.memoryCacheSize(2 * 1024 * 1024)
	// Maksymalny rozmiar bufora (13% dostępnej pamięci)
	.memoryCacheSizePercentage(13)
	
	// Strategia buforowania obrazków w systemie plików.
	.discCache(new UnlimitedDiscCache(cacheDir))
	// Maksymalny rozmiar plików (50 MB)
	.discCacheSize(50 * 1024 * 1024)
	// Maksymalna liczba plików (100)
	.discCacheFileCount(100)

	// Ustawia domyślne ustawienia wyświetlania (patrz DisplayImageOptions)
	.defaultDisplayImageOptions(displayOptions)

	.build();

Dostępnych jest kilka gotowych implementacji dla memoryCache(..):

  • LruMemoryCache w pierwszej kolejności usunięte z bufora zostną najdawniej wykorzystane bitmapy.
  • UsingFreqLimitedMemoryCache bufor zostanie czyszczony począwszy od najrzadziej używanych obrazków
  • FIFOLimitedMemoryCache wykorzystuje kolejkę FIFO przy usuwaniu bitmap z bufora.
  • LargestLimitedMemoryCache usuwanie zaczyna się od największych obrazków.
  • WeakMemoryCache wykorzystuje słabe referencje do przechowywania bitmap.

Strategie bufora dyskowego diskCache(..):

  • UnlimitedDiscCache brak ograniczeń dla plików bufora, jest to najszybszy mechanizm (domyślnie).
  • LruDiskCache umożliwia wprowadzenie ograniczenia rozmiaru lub/i maksymalnej liczby plików. W pierwszej kolejności usunięte zostną najdawniej wykorzystane pliki.
  • LimitedAgeDiscCache umożliwia wprowadzenie ograniczenia czasowego po jego przekroczeniu plik zostatnie usunięty.

Dodatkowe informacje

Buforowanie domyślnie jest wyłączone. Należy o tym pamiętać przy konfiguracji biblioteki. W celu minimalizacji wystąpień wyjątka OutOfMemoryError warto zastosować poniższe ustawienia:

  • Zmniejszenie wartości .threadPoolSize(..).
  • Użycie .bitmapConfig(Bitmap.Config.RGB_565) jeżeli nie wykorzystujemy przeźroczystości PNG.
  • Wykorzystanie .memoryCache(new WeakMemoryCache()) lub całkowite wyłączenie buforowania w pamięci .cacheInMemory(false).
  • Wybranie trybu .imageScaleType(ImageScaleType.IN_SAMPLE_INT).
  • Przy korzystaniu z ViewPager’a warto ustawić minimalną wartość .setOffscreenPageLimit(1).

Jeżeli w projekcie potrzebujemy kilka wersji ImageLoader’a (o różnych konfiguracjach: duże zdjęcia, miniaturki, itd.) możemy rozszerzyć klasę ImageLoader:

import com.nostra13.universalimageloader.core.ImageLoader;

public class BigPhotosLoader extends ImageLoader {

	private volatile static BigPhotosLoader instance;

	/** Returns singletone class instance */
	public static BigPhotosLoader getInstance() {
		if (instance == null) {
			synchronized (ImageLoader.class) {
				if (instance == null) {
					instance = new BigPhotosLoader();
				}
			}
		}
		return instance;
	}
}

f.lux

f.lux jest małym programem umożliwiającym zmianę kolorów wyświetlanych na ekranie w zależności od naszej lokalizacji i pory dnia. Przez dzień zastosowany jest wysoki kontrast, natomiast wieczorem kolory stają się bardziej stonowane. Dostępnych jest kilka gotowych ustawień, ale jeżeli nie odpowiadają one naszym upodobaniom, możemy wprowadzić własne. Polecam.

f.lux