RxJava — одна из самых популярных библиотек для переноса реактивного программирования на платформу Android, и в этой серии из трех частей я показал вам, как начать использовать эту библиотеку в ваших собственных проектах Android.
В Приступая к работе с RxJava 2 для Android мы рассмотрели, что такое RxJava и что он может предложить разработчикам Android, прежде чем создавать приложение Hello World, демонстрирующее три основных компонента RxJava: Observable
, Observer
и подписку.
В учебном пособии « Оперативное программирование в RxJava 2» мы рассмотрели, как выполнять сложные преобразования данных с использованием операторов, и как вы можете комбинировать Operator
и Scheduler
чтобы, наконец, сделать многопоточность на Android безболезненной.
Мы также коснулись RxAndroid, библиотеки, специально разработанной для того, чтобы помочь вам использовать RxJava в ваших проектах Android, но в RxAndroid есть еще много интересного. Итак, в этом посте я сосредоточусь исключительно на семействе библиотек RxAndroid.
Так же, как и RxJava, RxAndroid подвергся серьезной переработке в версии 2. Команда RxAndroid решила модулировать библиотеку, перенеся большую часть ее функциональности в выделенные дополнительные модули RxAndroid.
В этой статье я собираюсь показать вам, как настроить и использовать некоторые из самых популярных и мощных модулей RxAndroid, включая библиотеку, которая может сделать слушателей, обработчики и TextWatchers
делом прошлого, давая вам возможность обрабатывать любое событие Android UI в качестве Observable
.
А поскольку утечки памяти, вызванные неполными подписками, являются самым большим недостатком использования RxJava в ваших приложениях для Android, я также покажу вам, как использовать модуль RxAndroid, который может обрабатывать процесс подписки для вас. К концу этой статьи вы узнаете, как использовать RxJava в любом действии или Fragment
, не рискуя столкнуться с утечками памяти, связанными с RxJava.
Создание более активных интерфейсов Android
Реакция на события пользовательского интерфейса, такие как касания, пролистывания и ввод текста, является фундаментальной частью разработки практически любого приложения для Android, но обработка событий пользовательского интерфейса Android не особенно проста.
Обычно вы будете реагировать на события пользовательского интерфейса, используя комбинацию слушателей, обработчиков, TextWatchers
и, возможно, других компонентов, в зависимости от типа создаваемого вами пользовательского интерфейса. Каждый из этих компонентов требует от вас написания значительного количества стандартного кода, и, что еще хуже, нет последовательности в том, как вы реализуете эти различные компоненты. Например, вы обрабатываете события OnClick
, реализуя OnClickListener
:
1
2
3
4
5
6
7
|
Button button = (Button)findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//Perform some work//
}
});
|
Но это полностью отличается от того, как вы реализуете TextWatcher:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
final EditText name = (EditText) v.findViewById(R.id.name);
//Create a TextWatcher and specify that this TextWatcher should be called whenever the EditText’s content changes//
name.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
//Perform some work//
}
@Override
public void afterTextChanged(Editable s) {
}
});
|
Такое отсутствие согласованности может потенциально усложнить ваш код. И если у вас есть компоненты пользовательского интерфейса, которые зависят от вывода других компонентов пользовательского интерфейса, будьте готовы к тому, что все станет еще сложнее! Даже простой вариант использования — например, запрос пользователя ввести его имя в EditText
чтобы вы могли персонализировать текст, который появляется в последующих текстовых представлениях, — требует вложенных обратных вызовов, которые, как известно, трудно реализовать и поддерживать. (Некоторые люди называют вложенные обратные вызовы «адом обратного вызова».)
Ясно, что стандартизированный подход к обработке событий пользовательского интерфейса может значительно упростить ваш код, и RxBinding — это библиотека, предназначенная именно для этого, предоставляя привязки, которые позволяют преобразовывать любое событие Android View
в Observable
.
После того как вы преобразовали событие представления в Observable
, оно будет генерировать свои события пользовательского интерфейса в виде потоков данных, на которые вы можете подписаться точно так же, как вы подписались бы на любой другой Observable
.
Так как мы уже видели, как вы можете захватить событие щелчка, используя стандартный Android OnClickListener
, давайте посмотрим, как вы добьетесь тех же результатов, используя RxBinding:
1
2
3
4
5
6
7
8
9
|
import com.jakewharton.rxbinding.view.RxView;
…
Button button = (Button) findViewById(R.id.button);
RxView.clicks(button)
.subscribe(aVoid -> {
//Perform some work here//
});
|
Этот подход не только более лаконичен, но и является стандартной реализацией, которую можно применять ко всем событиям пользовательского интерфейса, которые происходят во всем приложении. Например, захват ввода текста происходит по той же схеме, что и захват событий кликов:
1
2
3
4
|
RxTextView.textChanges(editText)
.subscribe(charSequence -> {
//Perform some work here//
});
|
Пример приложения с RxBinding
Таким образом, вы можете точно увидеть, как RxBinding может упростить код, связанный с пользовательским интерфейсом вашего приложения, давайте создадим приложение, которое демонстрирует некоторые из этих привязок в действии. Я также собираюсь включить представление, зависящее от вывода другого View
, чтобы продемонстрировать, как RxBinding упрощает создание связей между компонентами пользовательского интерфейса.
Это приложение будет состоять из:
-
Button
которая отображаетToast
при нажатии. -
EditText
который обнаруживает изменения текста. -
TextView
который обновляется для отображения содержимогоEditText
.
Настройка проекта
Создайте проект Android Studio с настройками по вашему выбору, а затем откройте файл build.gradle уровня модуля и добавьте последнюю версию библиотеки RxBinding в качестве зависимости проекта. В целях минимизации стандартного кода я также собираюсь использовать лямбда-выражения, поэтому я обновил свой файл build.gradle для поддержки этой функции Java 8:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
apply plugin: ‘com.android.application’
android {
compileSdkVersion 25
buildToolsVersion «25.0.2»
defaultConfig {
applicationId «com.jessicathornsby.myapplication»
minSdkVersion 23
targetSdkVersion 25
versionCode 1
versionName «1.0»
testInstrumentationRunner «android.support.test.runner.AndroidJUnitRunner»
//Enable the Jack toolchain//
jackOptions {
enabled true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
}
}
//Set sourceCompatibility and targetCompatibility to JavaVersion.VERSION_1_8//
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
compile fileTree(dir: ‘libs’, include: [‘*.jar’])
androidTestCompile(‘com.android.support.test.espresso:espresso-core:2.2.2’, {
exclude group: ‘com.android.support’, module: ‘support-annotations’
})
//Add the core RxBinding library//
compile ‘com.jakewharton.rxbinding:rxbinding:0.4.0’
compile ‘com.android.support:appcompat-v7:25.3.0’
//Don’t forget to add the RxJava and RxAndroid dependencies//
compile ‘io.reactivex.rxjava2:rxandroid:2.0.1’
compile ‘io.reactivex.rxjava2:rxjava:2.0.5’
testCompile ‘junit:junit:4.12’
}
}
|
Когда вы работаете с несколькими библиотеками RxJava, во время компиляции вы можете столкнуться с дублирующимися файлами, скопированными в APK META-INF / DEPENDENCIES . Если вы сталкиваетесь с этой ошибкой, то обходной путь должен подавить эти дубликаты файлов, добавив следующее в ваш файл build.gradle уровня модуля :
1
2
3
4
5
6
7
|
android {
packagingOptions {
//Use “exclude” to point at the specific file (or files) that Android Studio is complaining about//
exclude ‘META-INF/rxjava.properties’
}
|
Создать основной макет деятельности
Синхронизируйте свои файлы Gradle, а затем создайте макет, состоящий из Button
, EditText
и TextView
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
<?xml version=»1.0″ encoding=»utf-8″?>
<LinearLayout 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»
android:orientation=»vertical»
tools:context=».MainActivity» >
<Button
android:text=»Button»
android:layout_width=»wrap_content»
android:layout_height=»wrap_content»
android:id=»@+id/button» />
<EditText
android:layout_width=»wrap_content»
android:layout_height=»wrap_content»
android:inputType=»textPersonName»
android:text=»Type here»
android:ems=»10″
android:id=»@+id/editText» />
<TextView
android:text=»TextView»
android:layout_width=»match_parent»
android:layout_height=»wrap_content»
android:id=»@+id/textView» />
</LinearLayout>
|
Кодировать привязки событий
Теперь давайте посмотрим, как вы будете использовать эти RxBinding для захвата различных событий пользовательского интерфейса, на которые наше приложение должно реагировать. Для начала объявите свой импорт и определите класс MainActivity
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
package com.jessicathornsby.myapplication;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
//Import the view.RxView class, so you can use RxView.clicks//
import com.jakewharton.rxbinding.view.RxView;
//Import widget.RxTextView so you can use RxTextView.textChanges//
import com.jakewharton.rxbinding.widget.RxTextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
TextView textView = (TextView) findViewById(R.id.textView);
EditText editText = (EditText) findViewById(R.id.editText);
//Code for the bindings goes here//
//…//
}
}
|
Теперь вы можете начать добавлять привязки для ответа на события пользовательского интерфейса. Метод RxView.clicks
используется для привязки событий щелчка. Создайте привязку для отображения тоста при каждом нажатии кнопки:
1
2
3
4
|
RxView.clicks(button)
.subscribe(aVoid -> {
Toast.makeText(MainActivity.this, «RxView.clicks», Toast.LENGTH_SHORT).show();
});
|
Затем используйте метод RxTextView.textChanges()
чтобы отреагировать на событие изменения текста, обновив TextView
содержимым нашего EditText
.
1
2
3
4
|
RxTextView.textChanges(editText)
.subscribe(charSequence -> {
textView.setText(charSequence);
});
|
Когда вы запустите свое приложение, вы увидите экран, подобный следующему.
Установите свой проект на физический смартфон или планшет Android или совместимый AVD, а затем потратьте некоторое время на взаимодействие с различными элементами пользовательского интерфейса. Ваше приложение должно реагировать на события нажатия и ввод текста как обычно — и все это без слушателя, TextWatcher или обратного вызова в поле зрения!
RxBinding для поддержки библиотек
Хотя базовая библиотека RxBinding обеспечивает привязки для всех элементов пользовательского интерфейса, составляющих стандартную платформу Android, существуют также родственные модули RxBinding, которые обеспечивают привязки для представлений, которые включены в состав различных библиотек поддержки Android.
Если вы добавили одну или несколько библиотек поддержки в свой проект, то вы, как правило, тоже захотите добавить соответствующий модуль RxBinding.
Эти родственные модули следуют прямому соглашению об именах, которое позволяет легко идентифицировать соответствующую библиотеку поддержки Android: каждый родственный модуль просто берет имя библиотеки поддержки и заменяет com.android
на com.jakewharton.rxbinding2:rxbinding
.
-
compile com.jakewharton.rxbinding2:rxbinding-recyclerview-v7:2.0.0'
-
compile 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
-
compile 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
-
compile 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
-
compile 'com.jakewharton.rxbinding2:rxbinding-recyclerview-v7:2.0.0'
-
compile 'com.jakewharton.rxbinding2:rxbinding-leanback-v17:2.0.0'
Если вы используете Kotlin в своих проектах Android , то для каждого модуля RxBinding также доступна версия Kotlin. Чтобы получить доступ к версии Kotlin, просто добавьте -kotlin
к имени библиотеки, с которой вы хотите работать, так:
1
|
compile ‘com.jakewharton.rxbinding2:rxbinding-design:2.0.0’
|
становится:
1
|
compile ‘com.jakewharton.rxbinding2:rxbinding-design-kotlin:2.0.0’
|
После того как вы преобразовали событие View
в Observable
, все эти события отправляются в виде потока данных. Как мы уже видели, вы можете подписаться на эти потоки, а затем выполнить любую задачу, которая необходима для запуска этого конкретного события пользовательского интерфейса, например, отображение Toast
или обновление TextView
. Однако вы также можете применить любой из огромного набора операторов RxJava к этому наблюдаемому потоку и даже объединить несколько операторов в цепочку для выполнения сложных преобразований событий вашего пользовательского интерфейса.
В одной статье слишком много операторов, чтобы их можно было обсудить (а официальные документы все равно перечисляют всех операторов ), но когда дело доходит до работы с событиями пользовательского интерфейса Android, есть несколько операторов, которые могут оказаться особенно полезными.
Оператор debounce()
Во-первых, если вы беспокоитесь о том, что нетерпеливый пользователь может многократно нажимать на элемент пользовательского интерфейса, что может запутать ваше приложение, то вы можете использовать оператор debounce()
для фильтрации любых событий пользовательского интерфейса, которые генерируются в быстрой последовательности.
В следующем примере я указываю, что эта кнопка должна реагировать на событие OnClick
только в том случае, если с момента предыдущего события щелчка был промежуток не менее 500 миллисекунд:
1
2
3
4
5
|
RxView.clicks(button)
.debounce(500, TimeUnit.MILLISECONDS)
.subscribe(aVoid -> {
Toast.makeText(MainActivity.this, «RxView.clicks», Toast.LENGTH_SHORT).show();
});
|
Оператор publish()
Вы также можете использовать оператор publish()
для присоединения нескольких слушателей к одному и тому же представлению, что традиционно сложно реализовать в Android.
Оператор publish()
преобразует стандартную Observable
в связную наблюдаемую. В то время как обычная наблюдаемая начинает излучать элементы, как только первый наблюдатель подписывается на нее, подключаемая наблюдаемая ничего не будет излучать, пока вы явно не проинструктируете ее, применив оператор connect()
. Это дает вам возможность для подписки нескольких наблюдателей без видимого начала испускания предметов, как только происходит первая подписка.
После того как вы создали все свои подписки, просто примените оператор connect()
и наблюдаемая начнет передавать данные всем назначенным наблюдателям.
Избегайте утечек памяти из приложений
Как мы видели в этой серии статей, RxJava может быть мощным инструментом для создания более активных, интерактивных приложений Android с гораздо меньшим количеством кода, чем обычно требуется для получения тех же результатов, используя только Java. Тем не менее, есть один существенный недостаток использования RxJava в ваших приложениях Android — вероятность утечек памяти, вызванных неполными подписками.
Эти утечки памяти происходят, когда система Android пытается уничтожить Activity
которая содержит работающую Observable
. Поскольку наблюдаемое работает, его наблюдатель все еще будет удерживать ссылку на действие, и в результате система не сможет собрать этот сборщик мусора.
Поскольку Android уничтожает и воссоздает Activity
каждый раз, когда изменяется конфигурация устройства, ваше приложение может создавать дублирующую Activity
каждый раз, когда пользователь переключается между портретным и ландшафтным режимами, а также каждый раз, когда они открывают и закрывают клавиатуру своего устройства.
Эти действия будут зависать в фоновом режиме, потенциально не собирая мусор. Поскольку действия представляют собой большие объекты, это может быстро привести к серьезным проблемам с управлением памятью, особенно с учетом того, что смартфоны и планшеты Android имеют ограниченную память. Сочетание большой утечки памяти и ограниченного объема памяти может быстро привести к ошибке Out Of Memory .
Утечки памяти в RxJava могут нанести ущерб производительности вашего приложения, но есть библиотека RxAndroid, которая позволяет использовать RxJava в вашем приложении, не беспокоясь об утечках памяти.
Библиотека RxLifecycle, разработанная Trello, предоставляет API-интерфейсы для обработки жизненного цикла, которые можно использовать для ограничения срока службы Observable
жизненным циклом Activity
или Fragment
. Как только это соединение установлено, RxLifecycle завершит последовательность наблюдаемой в ответ на события жизненного цикла, которые происходят в назначенной активности или фрагменте этой наблюдаемой. Это означает, что вы можете создать заметку, которая автоматически завершается всякий раз, когда действие или фрагмент уничтожается.
Обратите внимание, что мы говорим о прекращении последовательности, а не отписаться. Хотя о RxLifecycle часто говорят в контексте управления процессом подписки / отписки, технически он не отменяет подписку наблюдателя. Вместо этого библиотека RxLifecycle завершает наблюдаемую последовательность, испуская метод onComplete()
или onError()
. Когда вы отписываетесь, наблюдатель прекращает получать уведомления от своей наблюдаемой, даже если эта наблюдаемая все еще излучает предметы. Если вам требуется поведение отказа от подписки, то это то, что вам нужно реализовать самостоятельно.
Использование RxLifecycle
Чтобы использовать RxLifecycle в своих проектах Android, откройте файл build.gradle уровня модуля и добавьте последнюю версию библиотеки RxLifeycle , а также библиотеку RxLifecycle Android:
1
2
3
4
5
|
dependencies {
…
…
compile ‘com.trello.rxlifecycle2:rxlifecycle:2.0.1’
compile ‘com.trello.rxlifecycle2:rxlifecycle-android:2.0.1’
|
Затем в Activity
или Fragment
где вы хотите использовать API-интерфейсы обработки жизненного цикла библиотеки, расширьте RxActivity
, RxAppCompatActivity
или RxFragment
и добавьте соответствующий оператор импорта, например:
1
2
3
4
5
|
import com.trello.rxlifecycle2.components.support.RxAppCompatActivity;
…
public class MainActivity extends RxAppCompatActivity {
|
Когда дело доходит до привязки Observable
к жизненному циклу Activity
или Fragment
, вы можете либо указать событие жизненного цикла, в котором должно заканчиваться observable, либо позволить библиотеке RxLifecycle решить, когда она должна завершить наблюдаемую последовательность.
По умолчанию RxLifecycle прекратит наблюдаемое в дополнительном событии жизненного цикла с тем, где произошла эта подписка, поэтому, если вы подпишетесь на наблюдаемое во время onCreate()
вашей Activity, тогда RxLifecycle прекратит наблюдаемую последовательность во время onDestroy()
этого Activity , Если вы подписываетесь во время метода onAttach()
Fragment
, RxLifecycle прекратит эту последовательность в onDetach()
.
Вы можете оставить это решение до RxLifecycle, используя RxLifecycleAndroid.bindActivity
:
01
02
03
04
05
06
07
08
09
10
11
|
Observable<Integer> myObservable = Observable.range(0, 25);
…
@Override
public void onResume() {
super.onResume();
myObservable
.compose(RxLifecycleAndroid.bindActivity(lifecycle))
.subscribe();
}
|
В качестве альтернативы, вы можете указать событие жизненного цикла, где RxLifecycle должен завершить Observable
последовательность, используя RxLifecycle.bindUntilEvent
.
Здесь я указываю, что наблюдаемая последовательность должна заканчиваться в onDestroy()
:
1
2
3
4
5
6
7
|
@Override
public void onResume() {
super.onResume();
myObservable
.compose(RxLifecycle.bindUntilEvent(lifecycle, ActivityEvent.DESTROY))
.subscribe();
}
|
Работа с разрешениями Android Marshmallow
Последняя библиотека, которую мы рассмотрим, это RxPermissions, которая была разработана, чтобы помочь вам использовать RxJava с новой моделью разрешений, представленной в Android 6.0. Эта библиотека также позволяет вам отправлять запрос на разрешение и обрабатывать результат разрешения в том же месте, вместо того, чтобы запрашивать разрешение в одном месте, а затем обрабатывать его результаты отдельно, в Activity.onRequestPermissionsResult()
.
Начните с добавления библиотеки RxPermissions в ваш файл build.gradle :
1
|
compile ‘com.tbruyelle.rxpermissions2:rxpermissions:0.9.3@aar’
|
Затем создайте экземпляр RxPermissions:
1
|
RxPermissions rxPermissions = new RxPermissions(this);
|
Затем вы готовы начать отправлять запросы на разрешение через библиотеку RxPermissions, используя следующую формулу:
1
2
3
4
5
6
7
8
|
rxPermissions.request(Manifest.permission.READ_CONTACTS)
.subscribe(granted -> {
if (granted) {
// The permission has been granted//
} else {
// The permission has been denied//
}
});
|
Когда вы отправляете запрос на разрешение, крайне важно, так как всегда есть вероятность, что Activity
хостинга может быть уничтожена и затем воссоздана, пока диалог разрешений находится на экране, как правило, из-за изменения конфигурации, такого как переход пользователя в портретный и ландшафтный режимы. Если это произойдет, ваша подписка может быть не воссоздана, что означает, что вы не будете подписаны на наблюдаемую RxPermissions и не получите ответ пользователя на диалог запроса разрешения. Чтобы гарантировать, что ваше приложение получит ответ пользователя, всегда вызывайте ваш запрос во время фазы инициализации, такой как Activity.onCreate()
, Activity.onResume()
или View.onFinishInflate()
.
Для функций нередко требуется несколько разрешений. Например, отправка SMS-сообщения обычно требует, чтобы ваше приложение READ_CONTACTS
разрешения SEND_SMS
и READ_CONTACTS
. Библиотека RxPermissions предоставляет краткий метод выдачи нескольких запросов на разрешения, а затем объединяет ответы пользователя в один false
(одно или несколько разрешений было отклонено) или true
(все разрешения были предоставлены) ответ, на который можно затем соответствующим образом отреагировать.
01
02
03
04
05
06
07
08
09
10
|
RxPermissions.getInstance(this)
.request(Manifest.permission.SEND_SMS,
Manifest.permission.READ_CONTACTS)
.subscribe(granted -> {
if (granted) {
// All permissions were granted//
} else {
//One or more permissions was denied//
}
});
|
Как правило, вы хотите инициировать запрос на разрешение в ответ на событие пользовательского интерфейса, например, когда пользователь нажимает на элемент меню или кнопку, поэтому RxPermissions и RxBiding — это две библиотеки, которые особенно хорошо работают вместе.
Обрабатывая событие пользовательского интерфейса как наблюдаемое и делая запрос на разрешение через RxPermissions, вы можете выполнить большую работу с помощью всего лишь нескольких строк кода:
1
2
3
4
5
|
RxView.clicks(findViewById(R.id.enableBluetooth))
.compose(RxPermissions.getInstance(this).ensure(Manifest.permission.BLUETOOTH_ADMIN))
.subscribe(granted -> {
// The ‘enableBluetooth’ button has been clicked//
});
|
Вывод
После прочтения этой статьи у вас появятся некоторые идеи о том, как вырезать много стандартного кода из ваших приложений Android — используя RxJava для обработки всех событий пользовательского интерфейса вашего приложения и отправляя запросы на разрешение через RxPermissions. Мы также рассмотрели, как вы можете использовать RxJava в любой Activity
или Fragment
Android, не беспокоясь об утечках памяти, которые могут быть вызваны неполными подписками.
Мы исследовали некоторые из самых популярных и полезных библиотек RxJava и RxAndroid в этой серии, но если вы хотите узнать, что еще RxJava может предложить разработчикам Android, ознакомьтесь с некоторыми другими библиотеками RxAndroid. Вы найдете полный список дополнительных библиотек RxAndroid на GitHub.
А пока посмотрите другие наши посты о разработке для Android здесь на Envato Tuts +!