diff --git a/.gitignore b/.gitignore
index 63e899b..09b993d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,30 +1,8 @@
-# Built application files
-*.apk
-*.ap_
-
-# Files for the Dalvik VM
-*.dex
-
-# Java class files
-*.class
-
-# Generated files
-bin/
-gen/
-
-# Gradle files
-.gradle/
-build/
-/*/build/
-
-# Local configuration file (sdk path, etc)
-local.properties
-
-# Log Files
-*.log
-
-.gradle
-.idea
-.DS_Store
-/captures
*.iml
+.gradle
+/local.properties
+/.idea
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..4aebbba
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "BuildScripts"]
+ path = BuildScripts
+ url = git@github.com:TouchInstinct/BuildScripts.git
diff --git a/BuildScripts b/BuildScripts
new file mode 160000
index 0000000..3fe4b2a
--- /dev/null
+++ b/BuildScripts
@@ -0,0 +1 @@
+Subproject commit 3fe4b2a1aea149050b34c60ec7e2a876dfd8c0b2
diff --git a/api-logansquare/.gitignore b/api-logansquare/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/api-logansquare/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/api-logansquare/build.gradle b/api-logansquare/build.gradle
new file mode 100644
index 0000000..3e774b1
--- /dev/null
+++ b/api-logansquare/build.gradle
@@ -0,0 +1,23 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion versions.compileSdk
+
+ defaultConfig {
+ minSdkVersion versions.minSdk
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ api project(":storable")
+ api 'net.danlew:android.joda:2.9.9.4'
+
+ implementation "com.android.support:support-annotations:$versions.supportLibrary"
+ implementation "com.squareup.retrofit2:retrofit:$versions.retrofit"
+ implementation 'ru.touchin:logansquare:1.4.3'
+}
diff --git a/api-logansquare/src/main/AndroidManifest.xml b/api-logansquare/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d48ece2
--- /dev/null
+++ b/api-logansquare/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
diff --git a/api-logansquare/src/main/java/com/bluelinelabs/logansquare/ConverterUtils.java b/api-logansquare/src/main/java/com/bluelinelabs/logansquare/ConverterUtils.java
new file mode 100644
index 0000000..f8f7bba
--- /dev/null
+++ b/api-logansquare/src/main/java/com/bluelinelabs/logansquare/ConverterUtils.java
@@ -0,0 +1,22 @@
+package com.bluelinelabs.logansquare;
+
+import android.support.annotation.NonNull;
+
+import java.lang.reflect.Type;
+
+/**
+ * Utility class for the {@link ru.touchin.templates.logansquare.LoganSquareJsonFactory}. This resides in LoganSquare's
+ * main package in order to take advantage of the package-visible ConcreteParameterizedType class, which is essential
+ * to the support of generic classes in the Retrofit converter.
+ */
+public final class ConverterUtils {
+
+ @NonNull
+ public static ParameterizedType parameterizedTypeOf(@NonNull final Type type) {
+ return new ParameterizedType.ConcreteParameterizedType(type);
+ }
+
+ private ConverterUtils() {
+ }
+
+}
diff --git a/api-logansquare/src/main/java/ru/touchin/templates/ApiModel.java b/api-logansquare/src/main/java/ru/touchin/templates/ApiModel.java
new file mode 100644
index 0000000..5755445
--- /dev/null
+++ b/api-logansquare/src/main/java/ru/touchin/templates/ApiModel.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2016 Touch Instinct
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.templates;
+
+import android.support.annotation.CallSuper;
+import android.support.annotation.NonNull;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Iterator;
+
+import ru.touchin.roboswag.core.log.Lc;
+import ru.touchin.roboswag.core.log.LcGroup;
+
+/**
+ * Created by Gavriil Sitnikov on 11/08/2016.
+ * Just model from getting from API.
+ */
+public abstract class ApiModel implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Logging group to log API validation errors.
+ */
+ public static final LcGroup API_VALIDATION_LC_GROUP = new LcGroup("API_VALIDATION");
+
+ /**
+ * Validates list of objects. Use it if objects in list extends {@link ApiModel}.
+ *
+ * @param collection Collection of items to check;
+ * @param collectionValidationRule Rule explaining what to do if invalid items found;
+ * @throws ValidationException Exception of validation.
+ */
+ @SuppressWarnings({"PMD.PreserveStackTrace", "PMD.CyclomaticComplexity"})
+ // PreserveStackTrace: it's ok - we are logging it on Lc.e()
+ public static void validateCollection(@NonNull final Collection collection, @NonNull final CollectionValidationRule collectionValidationRule)
+ throws ValidationException {
+ boolean haveValidItem = false;
+ int position = 0;
+ final Iterator iterator = collection.iterator();
+ while (iterator.hasNext()) {
+ final Object item = iterator.next();
+ if (!(item instanceof ApiModel)) {
+ if (item != null) {
+ // let's just think that all of items are not ApiModels
+ break;
+ }
+ continue;
+ }
+
+ try {
+ ((ApiModel) item).validate();
+ haveValidItem = true;
+ } catch (final ValidationException exception) {
+ switch (collectionValidationRule) {
+ case EXCEPTION_IF_ANY_INVALID:
+ throw exception;
+ case EXCEPTION_IF_ALL_INVALID:
+ iterator.remove();
+ API_VALIDATION_LC_GROUP.e(exception, "Item %s is invalid at " + Lc.getCodePoint(null, 1), position);
+ if (!iterator.hasNext() && !haveValidItem) {
+ throw new ValidationException("Whole list is invalid at " + Lc.getCodePoint(null, 1));
+ }
+ break;
+ case REMOVE_INVALID_ITEMS:
+ iterator.remove();
+ API_VALIDATION_LC_GROUP.e(exception, "Item %s is invalid at " + Lc.getCodePoint(null, 1), position);
+ break;
+ default:
+ Lc.assertion("Unexpected rule " + collectionValidationRule);
+ break;
+ }
+ }
+ position++;
+ }
+ }
+
+ /**
+ * Validates collection on emptiness.
+ *
+ * @param collection Collection to check;
+ * @throws ValidationException Exception of validation.
+ */
+ protected static void validateCollectionNotEmpty(@NonNull final Collection collection)
+ throws ValidationException {
+ if (collection.isEmpty()) {
+ throw new ValidationException("List is empty at " + Lc.getCodePoint(null, 1));
+ }
+ }
+
+ /**
+ * Validates this object. Override it to write your own logic.
+ *
+ * @throws ValidationException Exception of validation.
+ */
+ @CallSuper
+ public void validate() throws ValidationException {
+ //do nothing
+ }
+
+ public enum CollectionValidationRule {
+ EXCEPTION_IF_ANY_INVALID,
+ EXCEPTION_IF_ALL_INVALID,
+ REMOVE_INVALID_ITEMS,
+ }
+
+ /**
+ * Class of exceptions throws during {@link ApiModel} validation.
+ */
+ public static class ValidationException extends IOException {
+
+ public ValidationException(@NonNull final String reason) {
+ super(reason);
+ }
+
+ }
+
+}
diff --git a/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareBigDecimalConverter.java b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareBigDecimalConverter.java
new file mode 100644
index 0000000..9f08d6f
--- /dev/null
+++ b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareBigDecimalConverter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2016 Touch Instinct
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.templates.logansquare;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.bluelinelabs.logansquare.typeconverters.TypeConverter;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+
+import ru.touchin.roboswag.core.log.Lc;
+
+/**
+ * LoganSquare converter for java.math.BigDecimal
+ */
+@SuppressWarnings("CPD-START") // similar to LoganSquareJodaTimeConverter
+public class LoganSquareBigDecimalConverter implements TypeConverter {
+
+ @Nullable
+ @Override
+ public BigDecimal parse(@NonNull final JsonParser jsonParser) throws IOException {
+ final String dateString = jsonParser.getValueAsString();
+ if (dateString == null) {
+ return null;
+ }
+ try {
+ return new BigDecimal(dateString);
+ } catch (final RuntimeException exception) {
+ Lc.assertion(exception);
+ }
+ return null;
+ }
+
+ @Override
+ public void serialize(@Nullable final BigDecimal object,
+ @Nullable final String fieldName,
+ final boolean writeFieldNameForObject,
+ @NonNull final JsonGenerator jsonGenerator)
+ throws IOException {
+ if (fieldName != null) {
+ jsonGenerator.writeStringField(fieldName, object != null ? object.toString() : null);
+ } else {
+ jsonGenerator.writeString(object != null ? object.toString() : null);
+ }
+ }
+
+}
+
diff --git a/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareEnum.java b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareEnum.java
new file mode 100644
index 0000000..a5d0963
--- /dev/null
+++ b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareEnum.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2016 Touch Instinct
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.templates.logansquare;
+
+import android.support.annotation.NonNull;
+
+/**
+ * Created by Gavriil Sitnikov.
+ * LoganSquare enum base class.
+ */
+public interface LoganSquareEnum {
+
+ @NonNull
+ String getValueName();
+
+}
diff --git a/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareEnumConverter.java b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareEnumConverter.java
new file mode 100644
index 0000000..c274667
--- /dev/null
+++ b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareEnumConverter.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2016 Touch Instinct
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.templates.logansquare;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter;
+
+import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
+
+/**
+ * Created by Gavriil Sitnikov.
+ * LoganSquare converter from String to Enum.
+ */
+@SuppressWarnings("PMD.UseVarargs")
+public class LoganSquareEnumConverter extends StringBasedTypeConverter {
+
+ @NonNull
+ private final T[] enumValues;
+ @Nullable
+ private final T defaultValue;
+
+ public LoganSquareEnumConverter(@NonNull final T[] enumValues) {
+ this(enumValues, null);
+ }
+
+ public LoganSquareEnumConverter(@NonNull final T[] enumValues, @Nullable final T defaultValue) {
+ super();
+ this.enumValues = enumValues;
+ this.defaultValue = defaultValue;
+ }
+
+ @Nullable
+ @Override
+ public T getFromString(@Nullable final String string) {
+ if (string == null) {
+ return defaultValue;
+ }
+ for (final T value : enumValues) {
+ if (value.getValueName().equals(string)) {
+ return value;
+ }
+ }
+ if (defaultValue != null) {
+ return defaultValue;
+ }
+ throw new ShouldNotHappenException("Enum parsing exception for value: " + string);
+ }
+
+ @Nullable
+ @Override
+ public String convertToString(@Nullable final T object) {
+ if (object == null) {
+ return null;
+ }
+ return object.getValueName();
+ }
+
+}
diff --git a/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJodaTimeConverter.java b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJodaTimeConverter.java
new file mode 100644
index 0000000..11f52b8
--- /dev/null
+++ b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJodaTimeConverter.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2016 Touch Instinct
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.templates.logansquare;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.bluelinelabs.logansquare.typeconverters.TypeConverter;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+
+import org.joda.time.DateTime;
+import org.joda.time.format.DateTimeFormatter;
+
+import java.io.IOException;
+
+import ru.touchin.roboswag.core.log.Lc;
+
+/**
+ * LoganSquare converter for joda.time.DateTime
+ */
+public class LoganSquareJodaTimeConverter implements TypeConverter {
+
+ @Nullable
+ private final DateTimeFormatter formatter;
+
+ public LoganSquareJodaTimeConverter() {
+ this.formatter = null;
+ }
+
+ public LoganSquareJodaTimeConverter(@Nullable final DateTimeFormatter formatter) {
+ this.formatter = formatter;
+ }
+
+ @Nullable
+ @Override
+ public DateTime parse(@NonNull final JsonParser jsonParser) throws IOException {
+ final String dateString = jsonParser.getValueAsString();
+ if (dateString == null || dateString.isEmpty()) {
+ return null;
+ }
+ try {
+ return DateTime.parse(dateString);
+ } catch (final RuntimeException exception) {
+ Lc.assertion(exception);
+ }
+ return null;
+ }
+
+ @Override
+ public void serialize(
+ @Nullable final DateTime object,
+ @Nullable final String fieldName,
+ final boolean writeFieldNameForObject,
+ @NonNull final JsonGenerator jsonGenerator
+ ) throws IOException {
+ final String serializedValue = object != null ? object.toString(formatter) : null;
+ if (fieldName != null) {
+ jsonGenerator.writeStringField(fieldName, !TextUtils.isEmpty(serializedValue) ? serializedValue : null);
+ } else {
+ jsonGenerator.writeString(!TextUtils.isEmpty(serializedValue) ? serializedValue : null);
+ }
+ }
+
+}
diff --git a/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJsonFactory.java b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJsonFactory.java
new file mode 100644
index 0000000..20c5b15
--- /dev/null
+++ b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJsonFactory.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2016 Touch Instinct
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.templates.logansquare;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.bluelinelabs.logansquare.ConverterUtils;
+import com.bluelinelabs.logansquare.LoganSquare;
+import com.fasterxml.jackson.core.JsonGenerator;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Map;
+
+import okhttp3.RequestBody;
+import okhttp3.ResponseBody;
+import retrofit2.Converter;
+import retrofit2.Retrofit;
+import ru.touchin.templates.retrofit.JsonRequestBodyConverter;
+import ru.touchin.templates.retrofit.JsonResponseBodyConverter;
+
+/**
+ * Created by Gavriil Sitnikov on 2/06/2016.
+ * LoganSquareConverter class to use with {@link Retrofit} to parse and generate models based on Logan Square library.
+ */
+public class LoganSquareJsonFactory extends Converter.Factory {
+
+ @NonNull
+ @Override
+ public Converter responseBodyConverter(@NonNull final Type type,
+ @NonNull final Annotation[] annotations,
+ @NonNull final Retrofit retrofit) {
+ return new LoganSquareJsonResponseBodyConverter<>(type);
+ }
+
+ @NonNull
+ @Override
+ public Converter, RequestBody> requestBodyConverter(@NonNull final Type type,
+ @NonNull final Annotation[] parameterAnnotations,
+ @NonNull final Annotation[] methodAnnotations,
+ @NonNull final Retrofit retrofit) {
+ return new LoganSquareRequestBodyConverter<>();
+ }
+
+ @Nullable
+ @Override
+ public Converter, String> stringConverter(@NonNull final Type type, @NonNull final Annotation[] annotations, @NonNull final Retrofit retrofit) {
+ if (type instanceof Class && ((Class) type).getSuperclass() == Enum.class) {
+ return new LoganSquareStringEnumConverter<>();
+ } else {
+ return super.stringConverter(type, annotations, retrofit);
+ }
+ }
+
+ public static class LoganSquareJsonResponseBodyConverter extends JsonResponseBodyConverter {
+
+ @NonNull
+ private final Type type;
+
+ public LoganSquareJsonResponseBodyConverter(@NonNull final Type type) {
+ super();
+ this.type = type;
+ }
+
+ @SuppressWarnings("unchecked")
+ @NonNull
+ @Override
+ protected T parseResponse(@NonNull final ResponseBody value) throws IOException {
+ if (type instanceof ParameterizedType) {
+ final ParameterizedType parameterizedType = (ParameterizedType) type;
+ final Type[] typeArguments = parameterizedType.getActualTypeArguments();
+ final Type firstType = typeArguments[0];
+
+ final Type rawType = parameterizedType.getRawType();
+ if (rawType == Map.class) {
+ return (T) LoganSquare.parseMap(value.byteStream(), (Class>) typeArguments[1]);
+ } else if (rawType == List.class) {
+ return (T) LoganSquare.parseList(value.byteStream(), (Class>) firstType);
+ } else {
+ // Generics
+ return (T) LoganSquare.parse(value.byteStream(), ConverterUtils.parameterizedTypeOf(type));
+ }
+ } else {
+ return (T) LoganSquare.parse(value.byteStream(), (Class) type);
+ }
+ }
+
+ }
+
+ public static class LoganSquareRequestBodyConverter extends JsonRequestBodyConverter {
+
+ @Override
+ protected void writeValueToByteArray(@NonNull final T value, @NonNull final ByteArrayOutputStream byteArrayOutputStream) throws IOException {
+ LoganSquare.serialize(value, byteArrayOutputStream);
+ }
+
+ }
+
+ public static class LoganSquareStringEnumConverter implements Converter {
+
+ @Nullable
+ @SuppressWarnings({"unchecked", "TryFinallyCanBeTryWithResources"})
+ @Override
+ public String convert(@NonNull final T value) throws IOException {
+ final StringWriter writer = new StringWriter();
+ try {
+ final JsonGenerator generator = LoganSquare.JSON_FACTORY.createGenerator(writer);
+ LoganSquare.typeConverterFor((Class) value.getClass()).serialize(value, null, false, generator);
+ generator.close();
+ return writer.toString().replaceAll("\"", "");
+ } finally {
+ writer.close();
+ }
+ }
+
+ }
+
+}
diff --git a/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJsonModel.java b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJsonModel.java
new file mode 100644
index 0000000..ea67fbe
--- /dev/null
+++ b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJsonModel.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2016 Touch Instinct
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.templates.logansquare;
+
+import android.support.annotation.Nullable;
+
+import ru.touchin.roboswag.core.log.Lc;
+import ru.touchin.templates.ApiModel;
+
+/**
+ * Created by Gavriil Sitnikov.
+ * Just model from getting from API via LoganSquare.
+ */
+public abstract class LoganSquareJsonModel extends ApiModel {
+
+ /**
+ * Throws exception if object is missed or null.
+ *
+ * @param object Value of field to check;
+ * @throws ValidationException Exception of validation.
+ */
+ protected static void validateNotNull(@Nullable final Object object) throws ValidationException {
+ if (object == null) {
+ throw new ValidationException("Not nullable object is null or missed at " + Lc.getCodePoint(null, 1));
+ }
+ }
+
+}
diff --git a/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquarePreferences.java b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquarePreferences.java
new file mode 100644
index 0000000..763ccf0
--- /dev/null
+++ b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquarePreferences.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2016 Touch Instinct
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.templates.logansquare;
+
+import android.content.SharedPreferences;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.bluelinelabs.logansquare.LoganSquare;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.List;
+
+import ru.touchin.roboswag.components.utils.storables.PreferenceStore;
+import ru.touchin.roboswag.core.observables.storable.Converter;
+import ru.touchin.roboswag.core.observables.storable.NonNullStorable;
+import ru.touchin.roboswag.core.observables.storable.Storable;
+import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
+
+/**
+ * Created by Gavriil Sitnikov on 26/12/2016.
+ * Utility class to get {@link Storable} that is storing LoganSquare (Json) generated object into preferences.
+ */
+@SuppressWarnings("CPD-START")
+//CPD: it is same code as in GoogleJsonPreferences
+public final class LoganSquarePreferences {
+
+ @NonNull
+ public static Storable jsonStorable(@NonNull final String name,
+ @NonNull final Class jsonClass,
+ @NonNull final SharedPreferences preferences) {
+ return new Storable.Builder(name, jsonClass, String.class, new PreferenceStore<>(preferences), new JsonConverter<>())
+ .setObserveStrategy(Storable.ObserveStrategy.CACHE_ACTUAL_VALUE)
+ .build();
+ }
+
+ @NonNull
+ public static NonNullStorable jsonStorable(@NonNull final String name,
+ @NonNull final Class jsonClass,
+ @NonNull final SharedPreferences preferences,
+ @NonNull final T defaultValue) {
+ return new Storable.Builder(name, jsonClass, String.class, new PreferenceStore<>(preferences), new JsonConverter<>())
+ .setObserveStrategy(Storable.ObserveStrategy.CACHE_ACTUAL_VALUE)
+ .setDefaultValue(defaultValue)
+ .build();
+ }
+
+ @NonNull
+ public static Storable, String> jsonListStorable(@NonNull final String name,
+ @NonNull final Class jsonListItemClass,
+ @NonNull final SharedPreferences preferences) {
+ return new Storable.Builder<>(name, List.class, String.class, new PreferenceStore<>(preferences), new JsonListConverter<>(jsonListItemClass))
+ .setObserveStrategy(Storable.ObserveStrategy.CACHE_ACTUAL_VALUE)
+ .build();
+ }
+
+ @NonNull
+ public static NonNullStorable, String> jsonListStorable(@NonNull final String name,
+ @NonNull final Class jsonListItemClass,
+ @NonNull final SharedPreferences preferences,
+ @NonNull final List defaultValue) {
+ return new Storable.Builder<>(name, List.class, String.class, new PreferenceStore<>(preferences), new JsonListConverter<>(jsonListItemClass))
+ .setObserveStrategy(Storable.ObserveStrategy.CACHE_ACTUAL_VALUE)
+ .setDefaultValue(defaultValue)
+ .build();
+ }
+
+ private LoganSquarePreferences() {
+ }
+
+ public static class JsonConverter implements Converter {
+
+ @Nullable
+ @Override
+ public String toStoreObject(@NonNull final Type jsonObjectType, @NonNull final Type stringType, @Nullable final TJsonObject object) {
+ if (object == null) {
+ return null;
+ }
+ try {
+ return LoganSquare.serialize(object);
+ } catch (final IOException exception) {
+ throw new ShouldNotHappenException(exception);
+ }
+ }
+
+ @Nullable
+ @Override
+ @SuppressWarnings("unchecked")
+ public TJsonObject toObject(@NonNull final Type jsonObjectClass, @NonNull final Type storeObjectType, @Nullable final String storeValue) {
+ if (storeValue == null) {
+ return null;
+ }
+ try {
+ return LoganSquare.parse(storeValue, (Class) jsonObjectClass);
+ } catch (final IOException exception) {
+ throw new ShouldNotHappenException(exception);
+ }
+ }
+
+ }
+
+ public static class JsonListConverter implements Converter, String> {
+
+ @NonNull
+ private final Class itemClass;
+
+ public JsonListConverter(@NonNull final Class itemClass) {
+ this.itemClass = itemClass;
+ }
+
+ @Nullable
+ @Override
+ @SuppressWarnings("unchecked")
+ public String toStoreObject(@NonNull final Type jsonObjectType, @NonNull final Type stringType, @Nullable final List object) {
+ if (object == null) {
+ return null;
+ }
+ try {
+ return LoganSquare.serialize(object, itemClass);
+ } catch (final IOException exception) {
+ throw new ShouldNotHappenException(exception);
+ }
+ }
+
+ @Nullable
+ @Override
+ public List toObject(@NonNull final Type jsonObjectType, @NonNull final Type stringType, @Nullable final String storeValue) {
+ if (storeValue == null) {
+ return null;
+ }
+ try {
+ return LoganSquare.parseList(storeValue, itemClass);
+ } catch (final IOException exception) {
+ throw new ShouldNotHappenException(exception);
+ }
+ }
+
+ }
+
+}
diff --git a/api-logansquare/src/main/java/ru/touchin/templates/retrofit/JsonRequestBodyConverter.java b/api-logansquare/src/main/java/ru/touchin/templates/retrofit/JsonRequestBodyConverter.java
new file mode 100644
index 0000000..e0ea113
--- /dev/null
+++ b/api-logansquare/src/main/java/ru/touchin/templates/retrofit/JsonRequestBodyConverter.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2017 Touch Instinct
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.templates.retrofit;
+
+
+import android.support.annotation.NonNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+import retrofit2.Converter;
+import ru.touchin.templates.ApiModel;
+
+/**
+ * Created by Gavriil Sitnikov on 14/02/2017.
+ * Object to serialize bodies of remote requests for Retrofit.
+ *
+ * @param Type of body object.
+ */
+public abstract class JsonRequestBodyConverter implements Converter {
+
+ private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8");
+
+ @NonNull
+ @Override
+ public RequestBody convert(@NonNull final T value) throws IOException {
+ if (value instanceof ApiModel) {
+ ((ApiModel) value).validate();
+ }
+ final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ writeValueToByteArray(value, byteArrayOutputStream);
+ return RequestBody.create(MEDIA_TYPE, byteArrayOutputStream.toByteArray());
+ }
+
+ /**
+ * Serializing value to byte stream.
+ *
+ * @param value Value to serialize;
+ * @param byteArrayOutputStream Byte stream to write serialized bytes;
+ * @throws IOException Throws on serialization.
+ */
+ protected abstract void writeValueToByteArray(@NonNull T value, @NonNull ByteArrayOutputStream byteArrayOutputStream) throws IOException;
+
+}
diff --git a/api-logansquare/src/main/java/ru/touchin/templates/retrofit/JsonResponseBodyConverter.java b/api-logansquare/src/main/java/ru/touchin/templates/retrofit/JsonResponseBodyConverter.java
new file mode 100644
index 0000000..614c8e2
--- /dev/null
+++ b/api-logansquare/src/main/java/ru/touchin/templates/retrofit/JsonResponseBodyConverter.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2017 Touch Instinct
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.templates.retrofit;
+
+import android.support.annotation.NonNull;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.SocketException;
+import java.util.Collection;
+import java.util.Map;
+
+import javax.net.ssl.SSLException;
+
+import okhttp3.ResponseBody;
+import okhttp3.internal.http2.StreamResetException;
+import retrofit2.Converter;
+import ru.touchin.roboswag.core.log.Lc;
+import ru.touchin.templates.ApiModel;
+
+/**
+ * Created by Gavriil Sitnikov on 14/02/2017.
+ * Object to deserialize responses of remote requests from Retrofit.
+ *
+ * @param Type of response object.
+ */
+public abstract class JsonResponseBodyConverter implements Converter {
+
+ @SuppressWarnings("PMD.AvoidInstanceofChecksInCatchClause")
+ //AvoidInstanceofChecksInCatchClause: we just don't need assertion on specific exceptions
+ @NonNull
+ @Override
+ public T convert(@NonNull final ResponseBody value) throws IOException {
+ final T result;
+ try {
+ result = parseResponse(value);
+ } catch (final IOException exception) {
+ if (!(exception instanceof SocketException)
+ && !(exception instanceof InterruptedIOException)
+ && !(exception instanceof SSLException)
+ && !(exception instanceof StreamResetException)) {
+ Lc.assertion(exception);
+ }
+ throw exception;
+ } finally {
+ value.close();
+ }
+
+ if (result instanceof ApiModel) {
+ validateModel((ApiModel) result);
+ }
+ if (result instanceof Collection) {
+ validateCollection((Collection) result);
+ }
+ if (result instanceof Map) {
+ validateCollection(((Map) result).values());
+ }
+
+ return result;
+ }
+
+ private void validateModel(@NonNull final ApiModel result) throws IOException {
+ try {
+ result.validate();
+ } catch (final ApiModel.ValidationException validationException) {
+ Lc.assertion(validationException);
+ throw validationException;
+ }
+ }
+
+ private void validateCollection(@NonNull final Collection result) throws IOException {
+ try {
+ ApiModel.validateCollection(result, getValidateCollectionRule());
+ } catch (final ApiModel.ValidationException validationException) {
+ Lc.assertion(validationException);
+ throw validationException;
+ }
+ }
+
+ @NonNull
+ protected ApiModel.CollectionValidationRule getValidateCollectionRule() {
+ return ApiModel.CollectionValidationRule.EXCEPTION_IF_ANY_INVALID;
+ }
+
+ /**
+ * Parses response to specific object.
+ *
+ * @param value Response to parse;
+ * @return Parsed object;
+ * @throws IOException Throws during parsing.
+ */
+ @NonNull
+ protected abstract T parseResponse(@NonNull ResponseBody value) throws IOException;
+
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 38e245d..a720f4f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,24 +1,39 @@
-apply plugin: 'com.android.library'
-
-android {
- compileSdkVersion compileSdk
-
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+buildscript {
+ ext.kotlin_version = '1.2.61'
+ repositories {
+ google()
+ jcenter()
}
-
- defaultConfig {
- minSdkVersion 16
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.1.4'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath 'de.aaschmid:gradle-cpd-plugin:1.0'
}
}
-dependencies {
- api project(':libraries:core')
-
- compileOnly "com.android.support:appcompat-v7:$supportLibraryVersion"
- compileOnly "com.android.support:recyclerview-v7:$supportLibraryVersion"
-
- compileOnly "io.reactivex:rxandroid:$rxAndroidVersion"
- compileOnly "io.reactivex:rxjava:$rxJavaVersion"
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ maven { url "http://dl.bintray.com/touchin/touchin-tools" }
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
+
+ext {
+ versions = [
+ compileSdk : 27,
+ minSdk : 16,
+ supportLibrary: '27.1.1',
+ navigation : '1.0.0-alpha04',
+ lifecycle : '1.1.1',
+ dagger : '2.16',
+ retrofit : '2.4.0',
+ rxJava : '2.1.17',
+ rxAndroid : '2.0.2',
+ crashlytics : '2.9.4'
+ ]
}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..e30af81
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,13 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+org.gradle.parallel=true
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..7a3265e
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..190eeaa
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sun Aug 05 23:37:20 MSK 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/kotlin-extensions/.gitignore b/kotlin-extensions/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/kotlin-extensions/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/kotlin-extensions/build.gradle b/kotlin-extensions/build.gradle
new file mode 100644
index 0000000..bcc6129
--- /dev/null
+++ b/kotlin-extensions/build.gradle
@@ -0,0 +1,16 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion versions.compileSdk
+
+ defaultConfig {
+ minSdkVersion versions.minSdk
+ }
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+
+ implementation "com.android.support:recyclerview-v7:$versions.supportLibrary"
+}
diff --git a/kotlin-extensions/src/main/AndroidManifest.xml b/kotlin-extensions/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..25060ad
--- /dev/null
+++ b/kotlin-extensions/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
diff --git a/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/Delegates.kt b/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/Delegates.kt
new file mode 100644
index 0000000..050bd6b
--- /dev/null
+++ b/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/Delegates.kt
@@ -0,0 +1,24 @@
+package ru.touchin.roboswag.components.extensions
+
+import kotlin.properties.Delegates
+import kotlin.properties.ObservableProperty
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+/**
+ * Simple observable delegate only for notification of new value.
+ */
+inline fun Delegates.observable(
+ initialValue: T,
+ crossinline onChange: (newValue: T) -> Unit
+): ReadWriteProperty = object : ObservableProperty(initialValue) {
+ override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(newValue)
+}
+
+inline fun Delegates.distinctUntilChanged(
+ initialValue: T,
+ crossinline onChange: (newValue: T) -> Unit
+): ReadWriteProperty = object : ObservableProperty(initialValue) {
+ override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) =
+ if (newValue != null && oldValue != newValue) onChange(newValue) else Unit
+}
diff --git a/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/View.kt b/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/View.kt
new file mode 100644
index 0000000..9f4ddca
--- /dev/null
+++ b/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/View.kt
@@ -0,0 +1,20 @@
+package ru.touchin.roboswag.components.extensions
+
+import android.os.Build
+import android.view.View
+
+private const val RIPPLE_EFFECT_DELAY = 150L
+
+/**
+ * Sets click listener to view. On click it will call something after delay.
+ *
+ * @param delay Delay after which click listener will be called;
+ * @param listener Click listener.
+ */
+fun View.setOnRippleClickListener(delay: Long = RIPPLE_EFFECT_DELAY, listener: (View) -> Unit) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ setOnClickListener { view -> postDelayed({ if (hasWindowFocus()) listener(view) }, delay) }
+ } else {
+ setOnClickListener(listener)
+ }
+}
diff --git a/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/ViewHolder.kt b/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/ViewHolder.kt
new file mode 100644
index 0000000..db7165f
--- /dev/null
+++ b/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/ViewHolder.kt
@@ -0,0 +1,32 @@
+package ru.touchin.roboswag.components.extensions
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.drawable.Drawable
+import android.support.annotation.ColorInt
+import android.support.annotation.ColorRes
+import android.support.annotation.DrawableRes
+import android.support.annotation.IdRes
+import android.support.annotation.StringRes
+import android.support.v4.content.ContextCompat
+import android.support.v7.widget.RecyclerView
+import android.view.View
+
+fun RecyclerView.ViewHolder.findViewById(@IdRes resId: Int): T = itemView.findViewById(resId)
+
+val RecyclerView.ViewHolder.context: Context
+ get() = itemView.context
+
+fun RecyclerView.ViewHolder.getText(@StringRes resId: Int): CharSequence = context.getText(resId)
+
+fun RecyclerView.ViewHolder.getString(@StringRes resId: Int): String = context.getString(resId)
+
+@SuppressWarnings("SpreadOperator") // it's OK for small arrays
+fun RecyclerView.ViewHolder.getString(@StringRes resId: Int, vararg args: Any): String = context.getString(resId, *args)
+
+@ColorInt
+fun RecyclerView.ViewHolder.getColor(@ColorRes resId: Int): Int = ContextCompat.getColor(context, resId)
+
+fun RecyclerView.ViewHolder.getColorStateList(@ColorRes resId: Int): ColorStateList? = ContextCompat.getColorStateList(context, resId)
+
+fun RecyclerView.ViewHolder.getDrawable(@DrawableRes resId: Int): Drawable? = ContextCompat.getDrawable(context, resId)
diff --git a/lifecycle-common/.gitignore b/lifecycle-common/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/lifecycle-common/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/lifecycle-common/build.gradle b/lifecycle-common/build.gradle
new file mode 100644
index 0000000..e7ab339
--- /dev/null
+++ b/lifecycle-common/build.gradle
@@ -0,0 +1,27 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion versions.compileSdk
+
+ defaultConfig {
+ minSdkVersion versions.minSdk
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ api project(":navigation")
+
+ compileOnly "javax.inject:javax.inject:1"
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+
+ implementation "com.android.support:appcompat-v7:$versions.supportLibrary"
+
+ implementation "android.arch.lifecycle:extensions:$versions.lifecycle"
+}
diff --git a/lifecycle-common/src/main/AndroidManifest.xml b/lifecycle-common/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..26f2069
--- /dev/null
+++ b/lifecycle-common/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/lifecycle-common/src/main/java/ru/touchin/templates/livedata/SingleLiveEvent.kt b/lifecycle-common/src/main/java/ru/touchin/templates/livedata/SingleLiveEvent.kt
new file mode 100644
index 0000000..27b0a1d
--- /dev/null
+++ b/lifecycle-common/src/main/java/ru/touchin/templates/livedata/SingleLiveEvent.kt
@@ -0,0 +1,25 @@
+package ru.touchin.templates.livedata
+
+import android.arch.lifecycle.LifecycleOwner
+import android.arch.lifecycle.MutableLiveData
+import android.arch.lifecycle.Observer
+import java.util.concurrent.atomic.AtomicBoolean
+
+class SingleLiveEvent : MutableLiveData() {
+
+ private val pending = AtomicBoolean(false)
+
+ override fun observe(owner: LifecycleOwner, observer: Observer) {
+ super.observe(owner, Observer { value ->
+ if (pending.compareAndSet(true, false)) {
+ observer.onChanged(value)
+ }
+ })
+ }
+
+ override fun setValue(value: T) {
+ pending.set(true)
+ super.setValue(value)
+ }
+
+}
diff --git a/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/LifecycleViewModelProviders.kt b/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/LifecycleViewModelProviders.kt
new file mode 100644
index 0000000..41ee808
--- /dev/null
+++ b/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/LifecycleViewModelProviders.kt
@@ -0,0 +1,49 @@
+package ru.touchin.templates.viewmodel
+
+import android.app.Activity
+import android.arch.lifecycle.LifecycleOwner
+import android.arch.lifecycle.ViewModelProvider
+import android.arch.lifecycle.ViewModelProviders
+import android.support.v4.app.Fragment
+import android.support.v4.app.FragmentActivity
+import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController
+
+object LifecycleViewModelProviders {
+
+ /**
+ * Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given
+ * {@code lifecycleOwner} is alive. More detailed explanation is in {@link ViewModel}.
+ *
+ * It uses the given {@link Factory} to instantiate new ViewModels.
+ *
+ * @param lifecycleOwner a lifecycle owner, in whose scope ViewModels should be retained (ViewController, Fragment, Activity)
+ * @param factory a {@code Factory} to instantiate new ViewModels
+ * @return a ViewModelProvider instance
+ */
+ fun of(lifecycleOwner: LifecycleOwner, factory: ViewModelProvider.Factory = getViewModelFactory(lifecycleOwner)): ViewModelProvider =
+ when (lifecycleOwner) {
+ is ViewController<*, *> -> ViewModelProviders.of(lifecycleOwner.fragment, factory)
+ is Fragment -> ViewModelProviders.of(lifecycleOwner, factory)
+ is FragmentActivity -> ViewModelProviders.of(lifecycleOwner, factory)
+ else -> throw IllegalArgumentException("Not supported LifecycleOwner.")
+ }
+
+ /**
+ * Returns ViewModelProvider.Factory instance from current lifecycleOwner.
+ * Search #ViewModelFactoryProvider are produced according to priorities:
+ * 1. View controller;
+ * 2. Fragment;
+ * 3. Parent fragment recursively;
+ * 4. Activity;
+ * 5. Application.
+ */
+ fun getViewModelFactory(provider: Any): ViewModelProvider.Factory =
+ when (provider) {
+ is ViewModelFactoryProvider -> provider.viewModelFactory
+ is ViewController<*, *> -> getViewModelFactory(provider.fragment)
+ is Fragment -> getViewModelFactory(provider.parentFragment ?: provider.requireActivity())
+ is Activity -> getViewModelFactory(provider.application)
+ else -> throw IllegalArgumentException("View model factory not found.")
+ }
+
+}
diff --git a/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/ViewModelFactory.kt b/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/ViewModelFactory.kt
new file mode 100644
index 0000000..8061ba6
--- /dev/null
+++ b/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/ViewModelFactory.kt
@@ -0,0 +1,17 @@
+package ru.touchin.templates.viewmodel
+
+import android.arch.lifecycle.ViewModel
+import android.arch.lifecycle.ViewModelProvider
+
+import javax.inject.Inject
+import javax.inject.Provider
+
+class ViewModelFactory @Inject constructor(
+ private val creators: Map, @JvmSuppressWildcards Provider>
+) : ViewModelProvider.Factory {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T
+ = (creators[modelClass]?.get() as? T) ?: throw IllegalArgumentException("Unknown model class $modelClass")
+
+}
diff --git a/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/ViewModelFactoryProvider.kt b/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/ViewModelFactoryProvider.kt
new file mode 100644
index 0000000..b1684e2
--- /dev/null
+++ b/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/ViewModelFactoryProvider.kt
@@ -0,0 +1,7 @@
+package ru.touchin.templates.viewmodel
+
+interface ViewModelFactoryProvider {
+
+ val viewModelFactory: ViewModelFactory
+
+}
diff --git a/lifecycle-rx/.gitignore b/lifecycle-rx/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/lifecycle-rx/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/lifecycle-rx/build.gradle b/lifecycle-rx/build.gradle
new file mode 100644
index 0000000..0956030
--- /dev/null
+++ b/lifecycle-rx/build.gradle
@@ -0,0 +1,24 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion versions.compileSdk
+
+ defaultConfig {
+ minSdkVersion versions.minSdk
+ }
+}
+
+dependencies {
+ api project(":utils")
+ api project(":logging")
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+
+ implementation "com.android.support:appcompat-v7:$versions.supportLibrary"
+
+ implementation "android.arch.lifecycle:extensions:$versions.lifecycle"
+
+ implementation "io.reactivex.rxjava2:rxjava:$versions.rxJava"
+ implementation "io.reactivex.rxjava2:rxandroid:$versions.rxAndroid"
+}
diff --git a/lifecycle-rx/src/main/AndroidManifest.xml b/lifecycle-rx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9145647
--- /dev/null
+++ b/lifecycle-rx/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/lifecycle-rx/src/main/java/ru/touchin/livedata/destroyable/BaseDestroyable.kt b/lifecycle-rx/src/main/java/ru/touchin/livedata/destroyable/BaseDestroyable.kt
new file mode 100644
index 0000000..cca546b
--- /dev/null
+++ b/lifecycle-rx/src/main/java/ru/touchin/livedata/destroyable/BaseDestroyable.kt
@@ -0,0 +1,65 @@
+package ru.touchin.livedata.destroyable
+
+import io.reactivex.Completable
+import io.reactivex.Flowable
+import io.reactivex.Maybe
+import io.reactivex.Observable
+import io.reactivex.Single
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.disposables.Disposable
+
+/**
+ * Created by Oksana Pokrovskaya on 7/03/18.
+ * Simple implementation of [Destroyable]. Could be used to not implement interface but use such object inside.
+ */
+open class BaseDestroyable : Destroyable {
+
+ private val subscriptions = CompositeDisposable()
+
+ override fun clearSubscriptions() = subscriptions.clear()
+
+ /**
+ * Call it on parent's onDestroy method.
+ */
+ fun onDestroy() = subscriptions.dispose()
+
+ override fun Flowable.untilDestroy(
+ onNext: (T) -> Unit,
+ onError: (Throwable) -> Unit,
+ onComplete: () -> Unit
+ ): Disposable = observeOn(AndroidSchedulers.mainThread())
+ .subscribe(onNext, onError, onComplete)
+ .also { subscriptions.add(it) }
+
+ override fun Observable.untilDestroy(
+ onNext: (T) -> Unit,
+ onError: (Throwable) -> Unit,
+ onComplete: () -> Unit
+ ): Disposable = observeOn(AndroidSchedulers.mainThread())
+ .subscribe(onNext, onError, onComplete)
+ .also { subscriptions.add(it) }
+
+ override fun Single.untilDestroy(
+ onSuccess: (T) -> Unit,
+ onError: (Throwable) -> Unit
+ ): Disposable = observeOn(AndroidSchedulers.mainThread())
+ .subscribe(onSuccess, onError)
+ .also { subscriptions.add(it) }
+
+ override fun Completable.untilDestroy(
+ onComplete: () -> Unit,
+ onError: (Throwable) -> Unit
+ ): Disposable = observeOn(AndroidSchedulers.mainThread())
+ .subscribe(onComplete, onError)
+ .also { subscriptions.add(it) }
+
+ override fun Maybe.untilDestroy(
+ onSuccess: (T) -> Unit,
+ onError: (Throwable) -> Unit,
+ onComplete: () -> Unit
+ ): Disposable = observeOn(AndroidSchedulers.mainThread())
+ .subscribe(onSuccess, onError, onComplete)
+ .also { subscriptions.add(it) }
+
+}
diff --git a/lifecycle-rx/src/main/java/ru/touchin/livedata/destroyable/Destroyable.kt b/lifecycle-rx/src/main/java/ru/touchin/livedata/destroyable/Destroyable.kt
new file mode 100644
index 0000000..93dd9b8
--- /dev/null
+++ b/lifecycle-rx/src/main/java/ru/touchin/livedata/destroyable/Destroyable.kt
@@ -0,0 +1,108 @@
+package ru.touchin.livedata.destroyable
+
+import io.reactivex.Completable
+import io.reactivex.Flowable
+import io.reactivex.Maybe
+import io.reactivex.Observable
+import io.reactivex.Single
+import io.reactivex.disposables.Disposable
+import io.reactivex.internal.functions.Functions
+import ru.touchin.roboswag.core.log.Lc
+import ru.touchin.roboswag.core.utils.ShouldNotHappenException
+
+/**
+ * Created by Oksana Pokrovskaya on 7/03/18.
+ * Interface that should be implemented by ([android.arch.lifecycle.ViewModel] etc.)
+ * to not manually manage subscriptions.
+ * Use [.untilDestroy] method to subscribe to observable where you want and unsubscribe onDestroy.
+ */
+interface Destroyable {
+
+ companion object {
+ private fun getActionThrowableForAssertion(codePoint: String, method: String = "untilDestroy"): (Throwable) -> Unit = { throwable ->
+ Lc.assertion(ShouldNotHappenException("Unexpected error on $method at $codePoint", throwable))
+ }
+ }
+
+ /**
+ * Removes all subscriptions
+ */
+ fun clearSubscriptions()
+
+ /**
+ * Method should be used to guarantee that observable won't be subscribed after onDestroy.
+ * It is automatically subscribing to the observable and calls onNextAction and onErrorAction on observable events.
+ * Don't forget to process errors if observable can emit them.
+ *
+ * @param onNext Action which will raise on every [io.reactivex.Emitter.onNext] item;
+ * @param onError Action which will raise on every [io.reactivex.Emitter.onError] throwable;
+ * @param onComplete Action which will raise on every [io.reactivex.Emitter.onComplete] item;
+ * @return [Disposable] which is wrapping source observable to unsubscribe from it onDestroy.
+ */
+ fun Flowable.untilDestroy(
+ onNext: (T) -> Unit = Functions.emptyConsumer()::accept,
+ onError: (Throwable) -> Unit = getActionThrowableForAssertion(Lc.getCodePoint(this, 2)),
+ onComplete: () -> Unit = Functions.EMPTY_ACTION::run
+ ): Disposable
+
+ /**
+ * Method should be used to guarantee that observable won't be subscribed after onDestroy.
+ * It is automatically subscribing to the observable and calls onNextAction and onErrorAction on observable events.
+ * Don't forget to process errors if observable can emit them.
+ *
+ * @param onNext Action which will raise on every [io.reactivex.Emitter.onNext] item;
+ * @param onError Action which will raise on every [io.reactivex.Emitter.onError] throwable;
+ * @param onComplete Action which will raise on every [io.reactivex.Emitter.onComplete] item;
+ * @return [Disposable] which is wrapping source observable to unsubscribe from it onDestroy.
+ */
+ fun Observable.untilDestroy(
+ onNext: (T) -> Unit = Functions.emptyConsumer()::accept,
+ onError: (Throwable) -> Unit = getActionThrowableForAssertion(Lc.getCodePoint(this, 2)),
+ onComplete: () -> Unit = Functions.EMPTY_ACTION::run
+ ): Disposable
+
+ /**
+ * Method should be used to guarantee that single won't be subscribed after onDestroy.
+ * It is automatically subscribing to the single and calls onSuccessAction and onErrorAction on single events.
+ * Don't forget to process errors if single can emit them.
+ *
+ * @param onSuccess Action which will raise on every [io.reactivex.SingleEmitter.onSuccess] item;
+ * @param onError Action which will raise on every [io.reactivex.SingleEmitter.onError] throwable;
+ * @return [Disposable] which is wrapping source single to unsubscribe from it onDestroy.
+ */
+ fun Single.untilDestroy(
+ onSuccess: (T) -> Unit = Functions.emptyConsumer()::accept,
+ onError: (Throwable) -> Unit = getActionThrowableForAssertion(Lc.getCodePoint(this, 2))
+ ): Disposable
+
+ /**
+ * Method should be used to guarantee that completable won't be subscribed after onDestroy.
+ * It is automatically subscribing to the completable and calls onCompletedAction and onErrorAction on completable events.
+ * Don't forget to process errors if completable can emit them.
+ *
+ * @param onComplete Action which will raise on every [io.reactivex.CompletableEmitter.onComplete] item;
+ * @param onError Action which will raise on every [io.reactivex.CompletableEmitter.onError] throwable;
+ * @return [Disposable] which is wrapping source completable to unsubscribe from it onDestroy.
+ */
+ fun Completable.untilDestroy(
+ onComplete: () -> Unit = Functions.EMPTY_ACTION::run,
+ onError: (Throwable) -> Unit = getActionThrowableForAssertion(Lc.getCodePoint(this, 2))
+ ): Disposable
+
+ /**
+ * Method should be used to guarantee that maybe won't be subscribed after onDestroy.
+ * It is automatically subscribing to the maybe and calls onSuccessAction and onErrorAction on maybe events.
+ * Don't forget to process errors if completable can emit them.
+ *
+ * @param onSuccess Action which will raise on every [io.reactivex.MaybeEmitter.onSuccess] ()} item;
+ * @param onError Action which will raise on every [io.reactivex.MaybeEmitter.onError] throwable;
+ * @param onComplete Action which will raise on every [io.reactivex.MaybeEmitter.onComplete] item;
+ * @return [Disposable] which is wrapping source maybe to unsubscribe from it onDestroy.
+ */
+ fun Maybe.untilDestroy(
+ onSuccess: (T) -> Unit = Functions.emptyConsumer()::accept,
+ onError: (Throwable) -> Unit = getActionThrowableForAssertion(Lc.getCodePoint(this, 2)),
+ onComplete: () -> Unit = Functions.EMPTY_ACTION::run
+ ): Disposable
+
+}
diff --git a/lifecycle-rx/src/main/java/ru/touchin/livedata/dispatcher/BaseLiveDataDispatcher.kt b/lifecycle-rx/src/main/java/ru/touchin/livedata/dispatcher/BaseLiveDataDispatcher.kt
new file mode 100644
index 0000000..dc6f5f4
--- /dev/null
+++ b/lifecycle-rx/src/main/java/ru/touchin/livedata/dispatcher/BaseLiveDataDispatcher.kt
@@ -0,0 +1,57 @@
+package ru.touchin.livedata.dispatcher
+
+import android.arch.lifecycle.MutableLiveData
+import io.reactivex.Completable
+import io.reactivex.Flowable
+import io.reactivex.Maybe
+import io.reactivex.Observable
+import io.reactivex.Single
+import io.reactivex.disposables.Disposable
+import ru.touchin.livedata.destroyable.BaseDestroyable
+import ru.touchin.livedata.destroyable.Destroyable
+import ru.touchin.templates.livedata.event.CompletableEvent
+import ru.touchin.templates.livedata.event.MaybeEvent
+import ru.touchin.templates.livedata.event.ObservableEvent
+import ru.touchin.templates.livedata.event.SingleEvent
+
+class BaseLiveDataDispatcher(private val destroyable: BaseDestroyable = BaseDestroyable()) : LiveDataDispatcher, Destroyable by destroyable {
+
+ override fun Flowable.dispatchTo(liveData: MutableLiveData>): Disposable {
+ liveData.value = ObservableEvent.Loading(liveData.value?.data)
+ return untilDestroy(
+ { data -> liveData.value = ObservableEvent.Success(data) },
+ { throwable -> liveData.value = ObservableEvent.Error(throwable, liveData.value?.data) },
+ { liveData.value = ObservableEvent.Completed(liveData.value?.data) })
+ }
+
+ override fun Observable.dispatchTo(liveData: MutableLiveData>): Disposable {
+ liveData.value = ObservableEvent.Loading(liveData.value?.data)
+ return untilDestroy(
+ { data -> liveData.value = ObservableEvent.Success(data) },
+ { throwable -> liveData.value = ObservableEvent.Error(throwable, liveData.value?.data) },
+ { liveData.value = ObservableEvent.Completed(liveData.value?.data) })
+ }
+
+ override fun Single.dispatchTo(liveData: MutableLiveData>): Disposable {
+ liveData.value = SingleEvent.Loading(liveData.value?.data)
+ return untilDestroy(
+ { data -> liveData.value = SingleEvent.Success(data) },
+ { throwable -> liveData.value = SingleEvent.Error(throwable, liveData.value?.data) })
+ }
+
+ override fun Completable.dispatchTo(liveData: MutableLiveData): Disposable {
+ liveData.value = CompletableEvent.Loading
+ return untilDestroy(
+ { liveData.value = CompletableEvent.Completed },
+ { throwable -> liveData.value = CompletableEvent.Error(throwable) })
+ }
+
+ override fun Maybe.dispatchTo(liveData: MutableLiveData>): Disposable {
+ liveData.value = MaybeEvent.Loading(liveData.value?.data)
+ return untilDestroy(
+ { data -> liveData.value = MaybeEvent.Success(data) },
+ { throwable -> liveData.value = MaybeEvent.Error(throwable, liveData.value?.data) },
+ { liveData.value = MaybeEvent.Completed(liveData.value?.data) })
+ }
+
+}
diff --git a/lifecycle-rx/src/main/java/ru/touchin/livedata/dispatcher/LiveDataDispatcher.kt b/lifecycle-rx/src/main/java/ru/touchin/livedata/dispatcher/LiveDataDispatcher.kt
new file mode 100644
index 0000000..8ef3510
--- /dev/null
+++ b/lifecycle-rx/src/main/java/ru/touchin/livedata/dispatcher/LiveDataDispatcher.kt
@@ -0,0 +1,27 @@
+package ru.touchin.livedata.dispatcher
+
+import android.arch.lifecycle.MutableLiveData
+import io.reactivex.Completable
+import io.reactivex.Flowable
+import io.reactivex.Maybe
+import io.reactivex.Observable
+import io.reactivex.Single
+import io.reactivex.disposables.Disposable
+import ru.touchin.templates.livedata.event.CompletableEvent
+import ru.touchin.templates.livedata.event.MaybeEvent
+import ru.touchin.templates.livedata.event.ObservableEvent
+import ru.touchin.templates.livedata.event.SingleEvent
+
+interface LiveDataDispatcher {
+
+ fun Flowable.dispatchTo(liveData: MutableLiveData>): Disposable
+
+ fun Observable.dispatchTo(liveData: MutableLiveData>): Disposable
+
+ fun Single.dispatchTo(liveData: MutableLiveData>): Disposable
+
+ fun Completable.dispatchTo(liveData: MutableLiveData): Disposable
+
+ fun Maybe.dispatchTo(liveData: MutableLiveData>): Disposable
+
+}
diff --git a/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/CompletableEvent.kt b/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/CompletableEvent.kt
new file mode 100644
index 0000000..05fa013
--- /dev/null
+++ b/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/CompletableEvent.kt
@@ -0,0 +1,14 @@
+package ru.touchin.templates.livedata.event
+
+/**
+ * Event class that emits from [io.reactivex.Completable].
+ */
+sealed class CompletableEvent {
+
+ object Loading: CompletableEvent()
+
+ object Completed: CompletableEvent()
+
+ data class Error(val throwable: Throwable): CompletableEvent()
+
+}
diff --git a/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/MaybeEvent.kt b/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/MaybeEvent.kt
new file mode 100644
index 0000000..779fa5e
--- /dev/null
+++ b/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/MaybeEvent.kt
@@ -0,0 +1,16 @@
+package ru.touchin.templates.livedata.event
+
+/**
+ * Event class that emits from [io.reactivex.Maybe].
+ */
+sealed class MaybeEvent(open val data: T?) {
+
+ class Loading(data: T?): MaybeEvent(data)
+
+ class Success(override val data: T): MaybeEvent(data)
+
+ class Error(val throwable: Throwable, data: T?): MaybeEvent(data)
+
+ class Completed(data: T?): MaybeEvent(data)
+
+}
diff --git a/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/ObservableEvent.kt b/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/ObservableEvent.kt
new file mode 100644
index 0000000..02373d5
--- /dev/null
+++ b/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/ObservableEvent.kt
@@ -0,0 +1,16 @@
+package ru.touchin.templates.livedata.event
+
+/**
+ * Event class that emits from [io.reactivex.Observable].
+ */
+sealed class ObservableEvent(open val data: T?) {
+
+ class Loading(data: T? = null): ObservableEvent(data)
+
+ class Success(override val data: T): ObservableEvent(data)
+
+ class Error(val throwable: Throwable, data: T? = null): ObservableEvent(data)
+
+ class Completed(data: T? = null): ObservableEvent(data)
+
+}
diff --git a/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/SingleEvent.kt b/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/SingleEvent.kt
new file mode 100644
index 0000000..f023393
--- /dev/null
+++ b/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/SingleEvent.kt
@@ -0,0 +1,14 @@
+package ru.touchin.templates.livedata.event
+
+/**
+ * Event class that emits from [io.reactivex.Single].
+ */
+sealed class SingleEvent(open val data: T?) {
+
+ class Loading(data: T?): SingleEvent(data)
+
+ class Success(override val data: T): SingleEvent(data)
+
+ class Error(val throwable: Throwable, data: T?): SingleEvent(data)
+
+}
diff --git a/lifecycle-rx/src/main/java/ru/touchin/templates/viewmodel/RxViewModel.kt b/lifecycle-rx/src/main/java/ru/touchin/templates/viewmodel/RxViewModel.kt
new file mode 100644
index 0000000..e27a860
--- /dev/null
+++ b/lifecycle-rx/src/main/java/ru/touchin/templates/viewmodel/RxViewModel.kt
@@ -0,0 +1,24 @@
+package ru.touchin.templates.viewmodel
+
+import android.arch.lifecycle.ViewModel
+import android.support.annotation.CallSuper
+import ru.touchin.livedata.dispatcher.BaseLiveDataDispatcher
+import ru.touchin.livedata.dispatcher.LiveDataDispatcher
+import ru.touchin.livedata.destroyable.BaseDestroyable
+import ru.touchin.livedata.destroyable.Destroyable
+
+/**
+ * Base class of ViewModel with [io.reactivex.disposables.Disposable] handling.
+ */
+open class RxViewModel(
+ private val destroyable: BaseDestroyable = BaseDestroyable(),
+ private val liveDataDispatcher: BaseLiveDataDispatcher = BaseLiveDataDispatcher(destroyable)
+) : ViewModel(), Destroyable by destroyable, LiveDataDispatcher by liveDataDispatcher {
+
+ @CallSuper
+ override fun onCleared() {
+ super.onCleared()
+ destroyable.onDestroy()
+ }
+
+}
diff --git a/lifecycle-rx/src/main/res/values/strings.xml b/lifecycle-rx/src/main/res/values/strings.xml
new file mode 100644
index 0000000..b340b3d
--- /dev/null
+++ b/lifecycle-rx/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ lifecycle-rx
+
diff --git a/logging/.gitignore b/logging/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/logging/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/logging/build.gradle b/logging/build.gradle
new file mode 100644
index 0000000..69333c1
--- /dev/null
+++ b/logging/build.gradle
@@ -0,0 +1,18 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion versions.compileSdk
+
+ defaultConfig {
+ minSdkVersion versions.minSdk
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation "com.android.support:support-annotations:$versions.supportLibrary"
+}
diff --git a/logging/src/main/AndroidManifest.xml b/logging/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8ce26b3
--- /dev/null
+++ b/logging/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/logging/src/main/java/ru/touchin/roboswag/core/log/ConsoleLogProcessor.java b/logging/src/main/java/ru/touchin/roboswag/core/log/ConsoleLogProcessor.java
new file mode 100644
index 0000000..0c05b84
--- /dev/null
+++ b/logging/src/main/java/ru/touchin/roboswag/core/log/ConsoleLogProcessor.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.core.log;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+/**
+ * Created by Gavriil Sitnikov on 13/11/2015.
+ * Simple {@link LogProcessor} implementation which is logging messages to console (logcat).
+ */
+public class ConsoleLogProcessor extends LogProcessor {
+
+ private static final int MAX_LOG_LENGTH = 4000;
+
+ public ConsoleLogProcessor(@NonNull final LcLevel lclevel) {
+ super(lclevel);
+ }
+
+ @NonNull
+ private String normalize(@NonNull final String message) {
+ return message.replace("\r\n", "\n").replace("\0", "");
+ }
+
+ @Override
+ @SuppressWarnings({"WrongConstant", "LogConditional"})
+ //WrongConstant, LogConditional: level.getPriority() is not wrong constant!
+ public void processLogMessage(@NonNull final LcGroup group, @NonNull final LcLevel level,
+ @NonNull final String tag, @NonNull final String message, @Nullable final Throwable throwable) {
+ final String messageToLog = normalize(message + (throwable != null ? '\n' + Log.getStackTraceString(throwable) : ""));
+ final int length = messageToLog.length();
+ for (int i = 0; i < length; i++) {
+ int newline = messageToLog.indexOf('\n', i);
+ newline = newline != -1 ? newline : length;
+ do {
+ final int end = Math.min(newline, i + MAX_LOG_LENGTH);
+ Log.println(level.getPriority(), tag, messageToLog.substring(i, end));
+ i = end;
+ }
+ while (i < newline);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/logging/src/main/java/ru/touchin/roboswag/core/log/Lc.java b/logging/src/main/java/ru/touchin/roboswag/core/log/Lc.java
new file mode 100644
index 0000000..3d46668
--- /dev/null
+++ b/logging/src/main/java/ru/touchin/roboswag/core/log/Lc.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.core.log;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
+
+/**
+ * Created by Gavriil Sitnikov on 13/11/2015.
+ * General logging utility of RoboSwag library.
+ * You can initialize {@link LogProcessor} to intercept log messages and make decision how to show them.
+ * Also you can specify assertions behavior to manually make application more stable in production but intercept illegal states in some
+ * third-party tool to fix them later but not crash in production.
+ */
+@SuppressWarnings({"checkstyle:methodname", "PMD.ShortMethodName", "PMD.ShortClassName"})
+//MethodNameCheck,ShortMethodName: log methods better be 1-symbol
+public final class Lc {
+
+ public static final LcGroup GENERAL_LC_GROUP = new LcGroup("GENERAL");
+
+ public static final int STACK_TRACE_CODE_DEPTH;
+
+ private static boolean crashOnAssertions = true;
+ @NonNull
+ private static LogProcessor logProcessor = new ConsoleLogProcessor(LcLevel.ERROR);
+
+ static {
+ final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
+ int stackDepth;
+ for (stackDepth = 0; stackDepth < stackTrace.length; stackDepth++) {
+ if (stackTrace[stackDepth].getClassName().equals(Lc.class.getName())) {
+ break;
+ }
+ }
+ STACK_TRACE_CODE_DEPTH = stackDepth + 1;
+ }
+
+ /**
+ * Flag to crash application or pass it to {@link LogProcessor#processLogMessage(LcGroup, LcLevel, String, String, Throwable)}
+ * on specific {@link LcGroup#assertion(Throwable)} points of code.
+ *
+ * @return True if application should crash on assertion.
+ */
+ public static boolean isCrashOnAssertions() {
+ return crashOnAssertions;
+ }
+
+ /**
+ * Returns {@link LogProcessor} object to intercept incoming log messages (by default it returns {@link ConsoleLogProcessor}).
+ *
+ * @return Specific {@link LogProcessor}.
+ */
+ @NonNull
+ public static LogProcessor getLogProcessor() {
+ return logProcessor;
+ }
+
+ /**
+ * Initialize general logging behavior.
+ *
+ * @param logProcessor {@link LogProcessor} to intercept all log messages;
+ * @param crashOnAssertions Flag to crash application
+ * or pass it to {@link LogProcessor#processLogMessage(LcGroup, LcLevel, String, String, Throwable)}
+ * on specific {@link LcGroup#assertion(Throwable)} points of code.
+ */
+ public static void initialize(@NonNull final LogProcessor logProcessor, final boolean crashOnAssertions) {
+ Lc.crashOnAssertions = crashOnAssertions;
+ Lc.logProcessor = logProcessor;
+ }
+
+ /**
+ * Logs debug message via {@link #GENERAL_LC_GROUP}.
+ *
+ * @param message Message or format of message to log;
+ * @param args Arguments of formatted message.
+ */
+ public static void d(@NonNull final String message, @NonNull final Object... args) {
+ GENERAL_LC_GROUP.d(message, args);
+ }
+
+ /**
+ * Logs debug message via {@link #GENERAL_LC_GROUP}.
+ *
+ * @param throwable Exception to log;
+ * @param message Message or format of message to log;
+ * @param args Arguments of formatted message.
+ */
+ public static void d(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) {
+ GENERAL_LC_GROUP.d(throwable, message, args);
+ }
+
+ /**
+ * Logs info message via {@link #GENERAL_LC_GROUP}.
+ *
+ * @param message Message or format of message to log;
+ * @param args Arguments of formatted message.
+ */
+ public static void i(@NonNull final String message, @NonNull final Object... args) {
+ GENERAL_LC_GROUP.i(message, args);
+ }
+
+ /**
+ * Logs info message via {@link #GENERAL_LC_GROUP}.
+ *
+ * @param throwable Exception to log;
+ * @param message Message or format of message to log;
+ * @param args Arguments of formatted message.
+ */
+ public static void i(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) {
+ GENERAL_LC_GROUP.i(throwable, message, args);
+ }
+
+ /**
+ * Logs warning message via {@link #GENERAL_LC_GROUP}.
+ *
+ * @param message Message or format of message to log;
+ * @param args Arguments of formatted message.
+ */
+ public static void w(@NonNull final String message, @NonNull final Object... args) {
+ GENERAL_LC_GROUP.w(message, args);
+ }
+
+ /**
+ * Logs warning message via {@link #GENERAL_LC_GROUP}.
+ *
+ * @param throwable Exception to log;
+ * @param message Message or format of message to log;
+ * @param args Arguments of formatted message.
+ */
+ public static void w(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) {
+ GENERAL_LC_GROUP.w(throwable, message, args);
+ }
+
+ /**
+ * Logs error message via {@link #GENERAL_LC_GROUP}.
+ *
+ * @param message Message or format of message to log;
+ * @param args Arguments of formatted message.
+ */
+ public static void e(@NonNull final String message, @NonNull final Object... args) {
+ GENERAL_LC_GROUP.e(message, args);
+ }
+
+ /**
+ * Logs error message via {@link #GENERAL_LC_GROUP}.
+ *
+ * @param throwable Exception to log;
+ * @param message Message or format of message to log;
+ * @param args Arguments of formatted message.
+ */
+ public static void e(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) {
+ GENERAL_LC_GROUP.e(throwable, message, args);
+ }
+
+ /**
+ * Processes assertion. Normally it will throw {@link ShouldNotHappenException} and crash app.
+ * If it should crash or not is specified at {@link Lc#isCrashOnAssertions()}.
+ * In some cases crash on assertions should be switched off and assertion should be processed in {@link LogProcessor}.
+ * It is useful for example to not crash but log it as handled crash in Crashlitycs in production build.
+ *
+ * @param message Message that is describing assertion.
+ */
+ public static void assertion(@NonNull final String message) {
+ GENERAL_LC_GROUP.assertion(message);
+ }
+
+ /**
+ * Processes assertion. Normally it will throw {@link ShouldNotHappenException} and crash app.
+ * If it should crash or not is specified at {@link Lc#isCrashOnAssertions()}.
+ * In some cases crash on assertions should be switched off and assertion should be processed in {@link LogProcessor}.
+ * It is useful for example to not crash but log it as handled crash in Crashlitycs in production build.
+ *
+ * @param throwable Exception that is describing assertion.
+ */
+ public static void assertion(@NonNull final Throwable throwable) {
+ GENERAL_LC_GROUP.assertion(throwable);
+ }
+
+ /**
+ * Throws assertion on main thread (to avoid Rx exceptions e.g.) and cuts top causes by type of exception class.
+ *
+ * @param assertion Source throwable;
+ * @param exceptionsClassesToCut Classes which will be cut from top of causes stack of source throwable.
+ */
+ @SafeVarargs
+ public static void cutAssertion(@NonNull final Throwable assertion, @NonNull final Class extends Throwable>... exceptionsClassesToCut) {
+ new Handler(Looper.getMainLooper()).post(() -> {
+ final List processedExceptions = new ArrayList<>();
+ Throwable result = assertion;
+ boolean exceptionAssignableFromIgnores;
+ do {
+ exceptionAssignableFromIgnores = false;
+ processedExceptions.add(result);
+ for (final Class exceptionClass : exceptionsClassesToCut) {
+ if (result.getClass().isAssignableFrom(exceptionClass)) {
+ exceptionAssignableFromIgnores = true;
+ result = result.getCause();
+ break;
+ }
+ }
+ }
+ while (exceptionAssignableFromIgnores && result != null && !processedExceptions.contains(result));
+ Lc.assertion(result != null ? result : assertion);
+ });
+ }
+
+ /**
+ * Returns line of code from where this method called.
+ *
+ * @param caller Object who is calling for code point;
+ * @return String represents code point.
+ */
+ @NonNull
+ public static String getCodePoint(@Nullable final Object caller) {
+ return getCodePoint(caller, 1);
+ }
+
+ /**
+ * Returns line of code from where this method called.
+ *
+ * @param caller Object who is calling for code point;
+ * @param stackShift caller Shift of stack (e.g. 2 means two elements deeper);
+ * @return String represents code point.
+ */
+ @NonNull
+ public static String getCodePoint(@Nullable final Object caller, final int stackShift) {
+ final StackTraceElement traceElement = Thread.currentThread().getStackTrace()[STACK_TRACE_CODE_DEPTH + stackShift];
+ return traceElement.getMethodName() + '(' + traceElement.getFileName() + ':' + traceElement.getLineNumber() + ')'
+ + (caller != null ? " of object " + caller.getClass().getSimpleName() + '(' + Integer.toHexString(caller.hashCode()) + ')' : "");
+ }
+
+ /**
+ * Prints stacktrace in log with specified tag.
+ *
+ * @param tag Tag to be shown in logs.
+ */
+
+ @SuppressLint("LogConditional")
+ public static void printStackTrace(@NonNull final String tag) {
+ final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
+ if (Log.isLoggable(tag, Log.DEBUG)) {
+ Log.d(tag, TextUtils.join("\n", Arrays.copyOfRange(stackTrace, STACK_TRACE_CODE_DEPTH, stackTrace.length)));
+ }
+ }
+
+ private Lc() {
+ }
+
+}
diff --git a/logging/src/main/java/ru/touchin/roboswag/core/log/LcGroup.java b/logging/src/main/java/ru/touchin/roboswag/core/log/LcGroup.java
new file mode 100644
index 0000000..3ccf1fa
--- /dev/null
+++ b/logging/src/main/java/ru/touchin/roboswag/core/log/LcGroup.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.core.log;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
+import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
+import ru.touchin.roboswag.core.utils.ThreadLocalValue;
+
+/**
+ * Created by Gavriil Sitnikov on 14/05/2016.
+ * Group of log messages with specific tag prefix (name of group).
+ * It could be used in specific {@link LogProcessor} to filter messages by group.
+ */
+@SuppressWarnings({"checkstyle:methodname", "PMD.ShortMethodName"})
+//MethodNameCheck,ShortMethodName: log methods better be 1-symbol
+public class LcGroup {
+
+ /**
+ * Logging group to log UI metrics (like inflation or layout time etc.).
+ */
+ public static final LcGroup UI_METRICS = new LcGroup("UI_METRICS");
+ /**
+ * Logging group to log UI lifecycle (onCreate, onStart, onResume etc.).
+ */
+ public static final LcGroup UI_LIFECYCLE = new LcGroup("UI_LIFECYCLE");
+
+ private static final ThreadLocalValue DATE_TIME_FORMATTER
+ = new ThreadLocalValue<>(() -> new SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()));
+
+ @NonNull
+ private final String name;
+ private boolean disabled;
+
+ public LcGroup(@NonNull final String name) {
+ this.name = name;
+ }
+
+ /**
+ * Disables logging of this group.
+ */
+ public void disable() {
+ disabled = true;
+ }
+
+ /**
+ * Enables logging of this group.
+ */
+ public void enable() {
+ disabled = false;
+ }
+
+ @NonNull
+ private String createLogTag() {
+ final StackTraceElement trace = Thread.currentThread().getStackTrace()[Lc.STACK_TRACE_CODE_DEPTH + 3];
+ return trace.getFileName() + ':' + trace.getLineNumber();
+ }
+
+ @SuppressWarnings("PMD.AvoidCatchingThrowable")
+ //AvoidCatchingThrowable: it is needed to safety format message
+ @Nullable
+ private String createFormattedMessage(@Nullable final String message, @NonNull final Object... args) {
+ try {
+ if (args.length > 0 && message == null) {
+ throw new ShouldNotHappenException("Args are not empty but format message is null");
+ }
+ return message != null ? (args.length > 0 ? String.format(message, args) : message) : null;
+ } catch (final Throwable formattingException) {
+ Lc.assertion(formattingException);
+ return null;
+ }
+ }
+
+ @NonNull
+ private String createLogMessage(@Nullable final String formattedMessage) {
+ return DATE_TIME_FORMATTER.get().format(System.currentTimeMillis())
+ + ' ' + Thread.currentThread().getName()
+ + ' ' + name
+ + (formattedMessage != null ? (' ' + formattedMessage) : "");
+ }
+
+ private void logMessage(@NonNull final LcLevel logLevel, @Nullable final String message,
+ @Nullable final Throwable throwable, @NonNull final Object... args) {
+ if (disabled || logLevel.lessThan(Lc.getLogProcessor().getMinLogLevel())) {
+ return;
+ }
+
+ if (throwable == null && args.length > 0 && args[0] instanceof Throwable) {
+ Lc.w("Maybe you've misplaced exception with first format arg? format: %s; arg: %s", message, args[0]);
+ }
+
+ final String formattedMessage = createFormattedMessage(message, args);
+ if (logLevel == LcLevel.ASSERT && Lc.isCrashOnAssertions()) {
+ throw createAssertion(formattedMessage, throwable);
+ }
+
+ Lc.getLogProcessor().processLogMessage(this, logLevel, createLogTag(), createLogMessage(formattedMessage), throwable);
+ }
+
+ @NonNull
+ private ShouldNotHappenException createAssertion(@Nullable final String message, @Nullable final Throwable exception) {
+ return exception != null
+ ? (message != null ? new ShouldNotHappenException(message, exception)
+ : (exception instanceof ShouldNotHappenException ? (ShouldNotHappenException) exception : new ShouldNotHappenException(exception)))
+ : (message != null ? new ShouldNotHappenException(message) : new ShouldNotHappenException());
+ }
+
+ /**
+ * Logs debug message.
+ *
+ * @param message Message or format of message to log;
+ * @param args Arguments of formatted message.
+ */
+ public void d(@NonNull final String message, @NonNull final Object... args) {
+ logMessage(LcLevel.DEBUG, message, null, args);
+ }
+
+ /**
+ * Logs debug message.
+ *
+ * @param throwable Exception to log;
+ * @param message Message or format of message to log;
+ * @param args Arguments of formatted message.
+ */
+ public void d(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) {
+ logMessage(LcLevel.DEBUG, message, throwable, args);
+ }
+
+ /**
+ * Logs info message.
+ *
+ * @param message Message or format of message to log;
+ * @param args Arguments of formatted message.
+ */
+ public void i(@NonNull final String message, @NonNull final Object... args) {
+ logMessage(LcLevel.INFO, message, null, args);
+ }
+
+ /**
+ * Logs info message.
+ *
+ * @param throwable Exception to log;
+ * @param message Message or format of message to log;
+ * @param args Arguments of formatted message.
+ */
+ public void i(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) {
+ logMessage(LcLevel.INFO, message, throwable, args);
+ }
+
+ /**
+ * Logs warning message.
+ *
+ * @param message Message or format of message to log;
+ * @param args Arguments of formatted message.
+ */
+ public void w(@NonNull final String message, @NonNull final Object... args) {
+ logMessage(LcLevel.WARN, message, null, args);
+ }
+
+ /**
+ * Logs warning message.
+ *
+ * @param throwable Exception to log;
+ * @param message Message or format of message to log;
+ * @param args Arguments of formatted message.
+ */
+ public void w(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) {
+ logMessage(LcLevel.WARN, message, throwable, args);
+ }
+
+ /**
+ * Logs error message.
+ *
+ * @param message Message or format of message to log;
+ * @param args Arguments of formatted message.
+ */
+ public void e(@NonNull final String message, @NonNull final Object... args) {
+ logMessage(LcLevel.ERROR, message, null, args);
+ }
+
+ /**
+ * Logs error message.
+ *
+ * @param throwable Exception to log;
+ * @param message Message or format of message to log;
+ * @param args Arguments of formatted message.
+ */
+ public void e(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) {
+ logMessage(LcLevel.ERROR, message, throwable, args);
+ }
+
+ /**
+ * Processes assertion. Normally it will throw {@link ShouldNotHappenException} and crash app.
+ * If it should crash or not is specified at {@link Lc#isCrashOnAssertions()}.
+ * In some cases crash on assertions should be switched off and assertion should be processed in {@link LogProcessor}.
+ * It is useful for example to not crash but log it as handled crash in Crashlitycs in production build.
+ *
+ * @param message Message that is describing assertion.
+ */
+ public void assertion(@NonNull final String message) {
+ logMessage(LcLevel.ASSERT, "Assertion appears at %s with message: %s", null, Lc.getCodePoint(null, 2), message);
+ }
+
+ /**
+ * Processes assertion. Normally it will throw {@link ShouldNotHappenException} and crash app.
+ * If it should crash or not is specified at {@link Lc#isCrashOnAssertions()}.
+ * In some cases crash on assertions should be switched off and assertion should be processed in {@link LogProcessor}.
+ * It is useful for example to not crash but log it as handled crash in Crashlitycs in production build.
+ *
+ * @param throwable Exception that is describing assertion.
+ */
+ public void assertion(@NonNull final Throwable throwable) {
+ logMessage(LcLevel.ASSERT, "Assertion appears at %s", throwable, Lc.getCodePoint(null, 2));
+ }
+
+}
diff --git a/logging/src/main/java/ru/touchin/roboswag/core/log/LcLevel.java b/logging/src/main/java/ru/touchin/roboswag/core/log/LcLevel.java
new file mode 100644
index 0000000..9dbd5e5
--- /dev/null
+++ b/logging/src/main/java/ru/touchin/roboswag/core/log/LcLevel.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.core.log;
+
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+/**
+ * Created by Gavriil Sitnikov on 14/05/2016.
+ * Level of log message.
+ */
+public enum LcLevel {
+
+ VERBOSE(Log.VERBOSE),
+ DEBUG(Log.DEBUG),
+ INFO(Log.INFO),
+ WARN(Log.WARN),
+ ERROR(Log.ERROR),
+ ASSERT(Log.ASSERT);
+
+ private final int priority;
+
+ LcLevel(final int priority) {
+ this.priority = priority;
+ }
+
+ /**
+ * Standard {@link Log} integer value of level represents priority of message.
+ *
+ * @return Integer level.
+ */
+ public int getPriority() {
+ return priority;
+ }
+
+ /**
+ * Compares priorities of LcLevels and returns if current is less than another.
+ *
+ * @param logLevel {@link LcLevel} to compare priority with;
+ * @return True if current level priority less than level passed as parameter.
+ */
+ public boolean lessThan(@NonNull final LcLevel logLevel) {
+ return this.priority < logLevel.priority;
+ }
+
+}
diff --git a/logging/src/main/java/ru/touchin/roboswag/core/log/LogProcessor.java b/logging/src/main/java/ru/touchin/roboswag/core/log/LogProcessor.java
new file mode 100644
index 0000000..6cb34a9
--- /dev/null
+++ b/logging/src/main/java/ru/touchin/roboswag/core/log/LogProcessor.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.core.log;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+/**
+ * Created by Gavriil Sitnikov on 13/11/2015.
+ * Abstract object to intercept log messages coming from {@link LcGroup} and {@link Lc} log methods.
+ */
+public abstract class LogProcessor {
+
+ @NonNull
+ private final LcLevel minLogLevel;
+
+ public LogProcessor(@NonNull final LcLevel minLogLevel) {
+ this.minLogLevel = minLogLevel;
+ }
+
+ /**
+ * Minimum logging level.
+ * Any messages with lower priority won't be passed into {@link #processLogMessage(LcGroup, LcLevel, String, String, Throwable)}.
+ *
+ * @return Minimum log level represented by {@link LcLevel} object.
+ */
+ @NonNull
+ public LcLevel getMinLogLevel() {
+ return minLogLevel;
+ }
+
+ /**
+ * Core method to process any incoming log messages from {@link LcGroup} and {@link Lc} with level higher or equals {@link #getMinLogLevel()}.
+ *
+ * @param group {@link LcGroup} where log message came from;
+ * @param level {@link LcLevel} level (priority) of message;
+ * @param tag String mark of message;
+ * @param message Message to log;
+ * @param throwable Exception to log.
+ */
+ public abstract void processLogMessage(@NonNull final LcGroup group, @NonNull final LcLevel level,
+ @NonNull final String tag, @NonNull final String message, @Nullable final Throwable throwable);
+
+}
diff --git a/logging/src/main/java/ru/touchin/roboswag/core/utils/ShouldNotHappenException.java b/logging/src/main/java/ru/touchin/roboswag/core/utils/ShouldNotHappenException.java
new file mode 100644
index 0000000..b3f11f0
--- /dev/null
+++ b/logging/src/main/java/ru/touchin/roboswag/core/utils/ShouldNotHappenException.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.core.utils;
+
+import android.support.annotation.NonNull;
+
+/**
+ * Created by Gavriil Sitnikov on 13/11/2015.
+ * Exception that should be threw when some unexpected code reached.
+ * E.g. if some value null but it is not legal or in default case in switch if all specific cases should be processed.
+ */
+public class ShouldNotHappenException extends RuntimeException {
+
+ private static final long serialVersionUID = 0;
+
+ public ShouldNotHappenException() {
+ super();
+ }
+
+ public ShouldNotHappenException(@NonNull final String detailMessage) {
+ super(detailMessage);
+ }
+
+ public ShouldNotHappenException(@NonNull final String detailMessage, @NonNull final Throwable throwable) {
+ super(detailMessage, throwable);
+ }
+
+ public ShouldNotHappenException(@NonNull final Throwable throwable) {
+ super(throwable);
+ }
+
+}
diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/OnFragmentStartedListener.java b/logging/src/main/java/ru/touchin/roboswag/core/utils/ThreadLocalValue.java
similarity index 51%
rename from src/main/java/ru/touchin/roboswag/components/navigation/OnFragmentStartedListener.java
rename to logging/src/main/java/ru/touchin/roboswag/core/utils/ThreadLocalValue.java
index 2dfb580..62cf778 100644
--- a/src/main/java/ru/touchin/roboswag/components/navigation/OnFragmentStartedListener.java
+++ b/logging/src/main/java/ru/touchin/roboswag/core/utils/ThreadLocalValue.java
@@ -17,23 +17,45 @@
*
*/
-package ru.touchin.roboswag.components.navigation;
+package ru.touchin.roboswag.core.utils;
import android.support.annotation.NonNull;
-import android.support.v4.app.Fragment;
/**
- * Created by Gavriil Sitnikov on 08/10/2014.
- * Base interface to listen child fragments start.
- * Usually it helps to determine that fragment have showed on screen and we can change {@link android.app.Activity}'s navigation state for example.
+ * Created by Gavriil Sitnikov on 13/11/2015.
+ * Thread local value with specified creator of value per thread.
*/
-public interface OnFragmentStartedListener {
+public class ThreadLocalValue extends ThreadLocal {
+
+ @NonNull
+ private final Fabric fabric;
+
+ public ThreadLocalValue(@NonNull final Fabric fabric) {
+ super();
+ this.fabric = fabric;
+ }
+
+ @NonNull
+ @Override
+ protected T initialValue() {
+ return fabric.create();
+ }
/**
- * Calls by child fragment (added via {@link android.support.v4.app.FragmentManager}) on it'sstart.
+ * Fabric of thread-local objects.
*
- * @param fragment Child fragment which called this method.
+ * @param Type of objects.
*/
- void onFragmentStarted(@NonNull Fragment fragment);
+ public interface Fabric {
-}
\ No newline at end of file
+ /**
+ * Creates object.
+ *
+ * @return new instance of object.
+ */
+ @NonNull
+ T create();
+
+ }
+
+}
diff --git a/modules.gradle b/modules.gradle
new file mode 100644
index 0000000..750126e
--- /dev/null
+++ b/modules.gradle
@@ -0,0 +1,30 @@
+final String rootDir
+if (gradle.ext.has('componentsRoot')) {
+ rootDir = gradle.ext['componentsRoot']
+} else {
+ rootDir = settingsDir
+}
+
+include ':logging'
+include ':utils'
+include ':navigation'
+include ':storable'
+include ':api-logansquare'
+include ':lifecycle-common'
+include ':lifecycle-rx'
+include ':views'
+include ':recyclerview-adapters'
+include ':kotlin-extensions'
+include ':templates'
+
+project(':utils').projectDir = new File(rootDir, 'utils')
+project(':logging').projectDir = new File(rootDir, 'logging')
+project(':navigation').projectDir = new File(rootDir, 'navigation')
+project(':storable').projectDir = new File(rootDir, 'storable')
+project(':api-logansquare').projectDir = new File(rootDir, 'api-logansquare')
+project(':lifecycle-common').projectDir = new File(rootDir, 'lifecycle-common')
+project(':lifecycle-rx').projectDir = new File(rootDir, 'lifecycle-rx')
+project(':views').projectDir = new File(rootDir, 'views')
+project(':recyclerview-adapters').projectDir = new File(rootDir, 'recyclerview-adapters')
+project(':kotlin-extensions').projectDir = new File(rootDir, 'kotlin-extensions')
+project(':templates').projectDir = new File(rootDir, 'templates')
diff --git a/navigation/.gitignore b/navigation/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/navigation/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/navigation/build.gradle b/navigation/build.gradle
new file mode 100644
index 0000000..8a95e1c
--- /dev/null
+++ b/navigation/build.gradle
@@ -0,0 +1,24 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion versions.compileSdk
+
+ defaultConfig {
+ minSdkVersion versions.minSdk
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ api project(":utils")
+ api project(":logging")
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+
+ implementation "com.android.support:appcompat-v7:$versions.supportLibrary"
+}
diff --git a/navigation/src/main/AndroidManifest.xml b/navigation/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..bd2d3ee
--- /dev/null
+++ b/navigation/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt
new file mode 100644
index 0000000..7bf87c5
--- /dev/null
+++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt
@@ -0,0 +1,263 @@
+/*
+ * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.components.navigation
+
+import android.content.Context
+import android.os.Bundle
+import android.support.annotation.IdRes
+import android.support.v4.app.Fragment
+import android.support.v4.app.FragmentManager
+import android.support.v4.app.FragmentTransaction
+import android.view.MenuItem
+
+import ru.touchin.roboswag.core.log.Lc
+
+/**
+ * Created by Gavriil Sitnikov on 07/03/2016.
+ * Navigation which is controlling fragments on activity using [android.support.v4.app.FragmentManager].
+ * Basically there are 4 main actions to add fragments to activity.
+ * 1) [.setInitial] means to set fragment on top and remove all previously added fragments from stack;
+ * 2) [.push] means to simply add fragment on top of the stack;
+ * 3) [.setAsTop] means to push fragment on top of the stack with specific [.TOP_FRAGMENT_TAG_MARK] tag.
+ * It is useful to realize up/back navigation: if [.up] method will be called then stack will go to nearest fragment with TOP tag.
+ * If [.back] method will be called then stack will go to previous fragment.
+ * Usually such logic using to set as top fragments from sidebar and show hamburger when some of them appeared;
+ * 4) [.pushForResult] means to push fragment with target fragment. It is also adding [.WITH_TARGET_FRAGMENT_TAG_MARK] tag.
+ * Also if such up/back navigation logic is not OK then [.backTo] method could be used with any condition to back to.
+ * In that case in any stack-change method it is allowed to setup fragment transactions.
+ */
+open class FragmentNavigation(
+ private val context: Context,
+ private val fragmentManager: FragmentManager,
+ @IdRes private val containerViewId: Int,
+ private val transition: Int = FragmentTransaction.TRANSIT_FRAGMENT_OPEN
+) {
+
+ companion object {
+ const val TOP_FRAGMENT_TAG_MARK = "TOP_FRAGMENT"
+ }
+
+ /**
+ * Returns if last fragment in stack is top (added by [.setAsTop] or [.setInitial]) like fragment from sidebar menu.
+ *
+ * @return True if last fragment on stack has TOP_FRAGMENT_TAG_MARK.
+ */
+ fun isCurrentFragmentTop(): Boolean {
+ if (fragmentManager.backStackEntryCount == 0) {
+ return true
+ }
+ val topFragmentTag = fragmentManager
+ .getBackStackEntryAt(fragmentManager.backStackEntryCount - 1)
+ .name
+ return topFragmentTag != null && topFragmentTag.contains(TOP_FRAGMENT_TAG_MARK)
+ }
+
+ /**
+ * Allowed to react on [android.app.Activity]'s menu item selection.
+ *
+ * @param item Selected menu item;
+ * @return True if reaction fired.
+ */
+ fun onOptionsItemSelected(item: MenuItem): Boolean = item.itemId == android.R.id.home && back()
+
+ /**
+ * Base method which is adding fragment to stack.
+ *
+ * @param fragmentClass Class of [Fragment] to instantiate;
+ * @param targetFragment Target fragment to be set as [Fragment.getTargetFragment] of instantiated [Fragment];
+ * @param addToStack Flag to add this transaction to the back stack;
+ * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment];
+ * @param backStackTag Tag of [Fragment] in back stack;
+ * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info.
+ */
+ fun addToStack(
+ fragmentClass: Class,
+ targetFragment: Fragment?,
+ targetRequestCode: Int,
+ addToStack: Boolean,
+ args: Bundle?,
+ backStackTag: String?,
+ transactionSetup: ((FragmentTransaction) -> Unit)?
+ ) {
+ if (fragmentManager.isDestroyed) {
+ Lc.assertion("FragmentManager is destroyed")
+ return
+ }
+
+ val fragment = Fragment.instantiate(context, fragmentClass.name, args)
+ if (targetFragment != null) {
+ if (fragmentManager !== targetFragment.fragmentManager) {
+ Lc.assertion("FragmentManager of target is differ then of creating fragment. Target will be lost after restoring activity. "
+ + targetFragment.fragmentManager + " != " + fragmentManager)
+ }
+ fragment.setTargetFragment(targetFragment, targetRequestCode)
+ }
+
+ val fragmentTransaction = fragmentManager.beginTransaction()
+ transactionSetup?.invoke(fragmentTransaction)
+ fragmentTransaction.replace(containerViewId, fragment, null)
+ if (addToStack) {
+ fragmentTransaction.addToBackStack(backStackTag).setTransition(transition)
+ } else {
+ fragmentTransaction.setPrimaryNavigationFragment(fragment)
+ }
+ fragmentTransaction.commit()
+ }
+
+ /**
+ * Simply calls [FragmentManager.popBackStack].
+ *
+ * @return True if it have back to some entry in stack.
+ */
+ fun back(): Boolean {
+ if (fragmentManager.backStackEntryCount >= 1) {
+ fragmentManager.popBackStack()
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Backs to fragment which back stack's entry satisfy to specific condition.
+ *
+ * @param condition Condition of back stack entry to be satisfied;
+ * @return True if it have back to some entry in stack.
+ */
+ fun backTo(condition: (FragmentManager.BackStackEntry) -> Boolean): Boolean {
+ val stackSize = fragmentManager.backStackEntryCount
+ var id: Int? = null
+ for (i in stackSize - 2 downTo 0) {
+ val backStackEntry = fragmentManager.getBackStackEntryAt(i)
+ if (condition(backStackEntry)) {
+ id = backStackEntry.id
+ break
+ }
+ }
+ if (id != null) {
+ fragmentManager.popBackStack(id, 0)
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Backs to fragment with specific [.TOP_FRAGMENT_TAG_MARK] tag.
+ * This tag is adding if fragment added to stack via [.setInitial] or [.setAsTop] methods.
+ * It can be used to create simple up/back navigation.
+ *
+ * @return True if it have back to some entry in stack.
+ */
+ fun up() {
+ if (!backTo { backStackEntry -> backStackEntry.name != null && backStackEntry.name.endsWith(TOP_FRAGMENT_TAG_MARK) }) {
+ fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
+ }
+ }
+
+ /**
+ * Pushes [Fragment] on top of stack with specific arguments and transaction setup.
+ *
+ * @param fragmentClass Class of [Fragment] to instantiate;
+ * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment];
+ * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info.
+ */
+ fun push(
+ fragmentClass: Class,
+ args: Bundle? = null,
+ addToStack: Boolean = true,
+ transactionSetup: ((FragmentTransaction) -> Unit)? = null
+ ) {
+ addToStack(fragmentClass, null, 0, addToStack, args, null, transactionSetup)
+ }
+
+ /**
+ * Pushes [Fragment] on top of stack with specific target fragment, arguments and transaction setup.
+ *
+ * @param fragmentClass Class of [Fragment] to instantiate;
+ * @param targetFragment Target fragment to be set as [Fragment.getTargetFragment] of instantiated [Fragment];
+ * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment];
+ * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info.
+ */
+ fun pushForResult(
+ fragmentClass: Class,
+ targetFragment: Fragment,
+ targetRequestCode: Int,
+ args: Bundle? = null,
+ transactionSetup: ((FragmentTransaction) -> Unit)? = null
+ ) {
+ addToStack(
+ fragmentClass,
+ targetFragment,
+ targetRequestCode,
+ true,
+ args,
+ null,
+ transactionSetup
+ )
+ }
+
+ /**
+ * Pushes [Fragment] on top of stack with specific transaction setup, arguments
+ * and with [.TOP_FRAGMENT_TAG_MARK] tag used for simple up/back navigation.
+ *
+ * @param fragmentClass Class of [Fragment] to instantiate;
+ * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment];
+ * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info.
+ */
+ fun setAsTop(
+ fragmentClass: Class,
+ args: Bundle? = null,
+ addToStack: Boolean = true,
+ transactionSetup: ((FragmentTransaction) -> Unit)? = null
+ ) {
+ addToStack(fragmentClass, null, 0, addToStack, args, "${fragmentClass.name};$TOP_FRAGMENT_TAG_MARK", transactionSetup)
+ }
+
+ /**
+ * Pops all [Fragment]s and places new initial [Fragment] on top of stack with specific transaction setup and arguments.
+ *
+ * @param fragmentClass Class of [Fragment] to instantiate;
+ * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment];
+ * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info.
+ */
+ @JvmOverloads
+ fun setInitial(
+ fragmentClass: Class,
+ args: Bundle? = null,
+ transactionSetup: ((FragmentTransaction) -> Unit)? = null
+ ) {
+ beforeSetInitialActions()
+ setAsTop(fragmentClass, args, false, transactionSetup)
+ }
+
+ /**
+ * Method calls every time before initial [Fragment] will be placed.
+ */
+ protected fun beforeSetInitialActions() {
+ if (fragmentManager.isDestroyed) {
+ Lc.assertion("FragmentManager is destroyed")
+ return
+ }
+
+ if (fragmentManager.backStackEntryCount > 0) {
+ fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
+ }
+ }
+
+}
diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/SerializableBundle.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/SerializableBundle.java
similarity index 100%
rename from src/main/java/ru/touchin/roboswag/components/navigation/SerializableBundle.java
rename to navigation/src/main/java/ru/touchin/roboswag/components/navigation/SerializableBundle.java
diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.java
similarity index 95%
rename from src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.java
rename to navigation/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.java
index 423fc98..090f9c0 100644
--- a/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.java
+++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.java
@@ -29,13 +29,15 @@ import android.view.MenuItem;
import android.view.View;
import ru.touchin.roboswag.components.navigation.activities.BaseActivity;
+import ru.touchin.roboswag.components.navigation.activities.OnBackPressedListener;
+import ru.touchin.roboswag.components.utils.UiUtils;
/**
* Created by Gavriil Sitnikov on 11/03/16.
* Simple realization of one-side {@link ActionBarDrawerToggle}.
*/
public class SimpleActionBarDrawerToggle extends ActionBarDrawerToggle
- implements FragmentManager.OnBackStackChangedListener, BaseActivity.OnBackPressedListener {
+ implements FragmentManager.OnBackStackChangedListener, OnBackPressedListener {
@NonNull
private final BaseActivity activity;
@@ -206,7 +208,7 @@ public class SimpleActionBarDrawerToggle extends ActionBarDrawerToggle
@Override
public void onDrawerClosed(@NonNull final View view) {
if (isInvalidateOptionsMenuSupported) {
- activity.supportInvalidateOptionsMenu();
+ activity.invalidateOptionsMenu();
}
}
@@ -221,9 +223,9 @@ public class SimpleActionBarDrawerToggle extends ActionBarDrawerToggle
@Override
public void onDrawerOpened(@NonNull final View drawerView) {
- activity.hideSoftInput();
+ UiUtils.OfViews.hideSoftInput(activity);
if (isInvalidateOptionsMenuSupported) {
- activity.supportInvalidateOptionsMenu();
+ activity.invalidateOptionsMenu();
}
}
@@ -245,4 +247,4 @@ public class SimpleActionBarDrawerToggle extends ActionBarDrawerToggle
super.onDrawerSlide(drawerView, this.slideOffset);
}
-}
\ No newline at end of file
+}
diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.java
new file mode 100644
index 0000000..2e09da4
--- /dev/null
+++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.components.navigation.activities;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.util.ArraySet;
+import android.support.v7.app.AppCompatActivity;
+
+import java.util.Set;
+
+import ru.touchin.roboswag.core.log.Lc;
+import ru.touchin.roboswag.core.log.LcGroup;
+
+/**
+ * Created by Gavriil Sitnikov on 08/03/2016.
+ * Base activity to use in components repository.
+ */
+public abstract class BaseActivity extends AppCompatActivity {
+
+ @NonNull
+ private final Set onBackPressedListeners = new ArraySet<>();
+
+ @Override
+ protected void onCreate(@Nullable final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ }
+
+ @Override
+ protected void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this) + " requestCode: " + requestCode + "; resultCode: " + resultCode);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ }
+
+ @Override
+ protected void onPause() {
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ super.onPause();
+ }
+
+ @Override
+ protected void onSaveInstanceState(@NonNull final Bundle stateToSave) {
+ super.onSaveInstanceState(stateToSave);
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ }
+
+ @Override
+ protected void onStop() {
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ super.onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ onBackPressed();
+ return true;
+ }
+
+ public void addOnBackPressedListener(@NonNull final OnBackPressedListener onBackPressedListener) {
+ onBackPressedListeners.add(onBackPressedListener);
+ }
+
+ public void removeOnBackPressedListener(@NonNull final OnBackPressedListener onBackPressedListener) {
+ onBackPressedListeners.remove(onBackPressedListener);
+ }
+
+ @Override
+ public void onBackPressed() {
+ for (final OnBackPressedListener onBackPressedListener : onBackPressedListeners) {
+ if (onBackPressedListener.onBackPressed()) {
+ return;
+ }
+ }
+ super.onBackPressed();
+ }
+
+}
diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/OnBackPressedListener.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/OnBackPressedListener.java
new file mode 100644
index 0000000..0f052f2
--- /dev/null
+++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/OnBackPressedListener.java
@@ -0,0 +1,15 @@
+package ru.touchin.roboswag.components.navigation.activities;
+
+/**
+ * Interface to be implemented for someone who want to intercept device back button pressing event.
+ */
+public interface OnBackPressedListener {
+
+ /**
+ * Calls when user presses device back button.
+ *
+ * @return True if it is processed by this object.
+ */
+ boolean onBackPressed();
+
+}
diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.java
new file mode 100644
index 0000000..cb38f7a
--- /dev/null
+++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.java
@@ -0,0 +1,430 @@
+/*
+ * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.components.navigation.fragments;
+
+import android.animation.Animator;
+import android.annotation.SuppressLint;
+import android.arch.lifecycle.Lifecycle;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Canvas;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.support.annotation.CallSuper;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.widget.FrameLayout;
+
+import java.lang.reflect.Constructor;
+
+import ru.touchin.roboswag.components.navigation.BuildConfig;
+import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController;
+import ru.touchin.roboswag.core.log.Lc;
+import ru.touchin.roboswag.core.log.LcGroup;
+import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
+
+/**
+ * Created by Gavriil Sitnikov on 21/10/2015.
+ * Fragment instantiated in specific activity of {@link TActivity} type that is holding {@link ViewController} inside.
+ *
+ * @param Type of object which is representing it's fragment state;
+ * @param Type of {@link FragmentActivity} where fragment could be attached to.
+ */
+@SuppressWarnings("PMD.TooManyMethods")
+public class ViewControllerFragment extends Fragment {
+
+ private static final String VIEW_CONTROLLER_CLASS_EXTRA = "VIEW_CONTROLLER_CLASS_EXTRA";
+ private static final String VIEW_CONTROLLER_STATE_EXTRA = "VIEW_CONTROLLER_STATE_EXTRA";
+
+ private static long acceptableUiCalculationTime = 100;
+
+ /**
+ * Sets acceptable UI calculation time so there will be warnings in logs if ViewController's inflate/layout actions will take more than that time.
+ * It's 100ms by default.
+ */
+ public static void setAcceptableUiCalculationTime(final long acceptableUiCalculationTime) {
+ ViewControllerFragment.acceptableUiCalculationTime = acceptableUiCalculationTime;
+ }
+
+ @NonNull
+ private static T reserialize(@NonNull final T parcelable) {
+ Parcel parcel = Parcel.obtain();
+ parcel.writeParcelable(parcelable, 0);
+ final byte[] serializableBytes = parcel.marshall();
+ parcel.recycle();
+ parcel = Parcel.obtain();
+ parcel.unmarshall(serializableBytes, 0, serializableBytes.length);
+ parcel.setDataPosition(0);
+ final T result = parcel.readParcelable(Thread.currentThread().getContextClassLoader());
+ parcel.recycle();
+ return result;
+ }
+
+ /**
+ * Creates {@link Bundle} which will store state.
+ *
+ * @param state State to use into ViewController.
+ * @return Returns bundle with state inside.
+ */
+ @NonNull
+ public static Bundle args(@NonNull final Class extends ViewController> viewControllerClass, @Nullable final Parcelable state) {
+ final Bundle result = new Bundle();
+ result.putSerializable(VIEW_CONTROLLER_CLASS_EXTRA, viewControllerClass);
+ result.putParcelable(VIEW_CONTROLLER_STATE_EXTRA, state);
+ return result;
+ }
+
+ @Nullable
+ private ViewController viewController;
+ private Class> viewControllerClass;
+ private TState state;
+ @Nullable
+ private ActivityResult pendingActivityResult;
+
+ private boolean appeared;
+
+ /**
+ * Returns specific {@link Parcelable} which contains state of fragment and it's {@link ViewController}.
+ *
+ * @return Object represents state.
+ */
+ @NonNull
+ public TState getState() {
+ return state;
+ }
+
+ @NonNull
+ public Class> getViewControllerClass() {
+ return viewControllerClass;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onCreate(@Nullable final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setHasOptionsMenu(!isChildFragment());
+
+ viewControllerClass = (Class>) getArguments().getSerializable(VIEW_CONTROLLER_CLASS_EXTRA);
+ state = savedInstanceState != null
+ ? savedInstanceState.getParcelable(VIEW_CONTROLLER_STATE_EXTRA)
+ : (getArguments() != null ? getArguments().getParcelable(VIEW_CONTROLLER_STATE_EXTRA) : null);
+ if (state != null) {
+ if (BuildConfig.DEBUG) {
+ state = reserialize(state);
+ }
+ } else {
+ Lc.assertion("State is required and null");
+ }
+ }
+
+ @NonNull
+ @Override
+ public final View onCreateView(
+ @NonNull final LayoutInflater inflater,
+ @Nullable final ViewGroup container,
+ @Nullable final Bundle savedInstanceState
+ ) {
+ return new PlaceholderView(inflater.getContext(), viewControllerClass.getName());
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ //noinspection ConstantConditions
+ viewController = createViewController(requireActivity(), (ViewGroup) getView(), savedInstanceState);
+ viewController.onCreate();
+ if (pendingActivityResult != null) {
+ viewController.onActivityResult(pendingActivityResult.requestCode, pendingActivityResult.resultCode, pendingActivityResult.data);
+ pendingActivityResult = null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public Animation onCreateAnimation(final int transit, final boolean enter, final int nextAnim) {
+ if (viewController != null) {
+ return viewController.onCreateAnimation(transit, enter, nextAnim);
+ } else {
+ return null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public Animator onCreateAnimator(final int transit, final boolean enter, final int nextAnim) {
+ if (viewController != null) {
+ return viewController.onCreateAnimator(transit, enter, nextAnim);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
+ super.onViewStateRestored(savedInstanceState);
+ if (viewController != null) {
+ viewController.onViewStateRestored(savedInstanceState);
+ }
+ }
+
+ @SuppressLint("RestrictedApi")
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (!appeared && isMenuVisible()) {
+ onAppear();
+ }
+ if (viewController != null) {
+ viewController.onStart();
+ }
+ }
+
+ /**
+ * Called when fragment is moved in started state and it's {@link #isMenuVisible()} sets to true.
+ * Usually it is indicating that user can't see fragment on screen and useful to track analytics events.
+ */
+ @CallSuper
+ protected void onAppear() {
+ appeared = true;
+ if (viewController != null) {
+ viewController.onAppear();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (viewController != null) {
+ viewController.onResume();
+ }
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ if (viewController != null) {
+ viewController.onLowMemory();
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ if (viewController != null) {
+ viewController.onCreateOptionsMenu(menu, inflater);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
+ return (viewController != null && viewController.onOptionsItemSelected(item)) || super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (viewController != null) {
+ viewController.onPause();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) {
+ super.onSaveInstanceState(savedInstanceState);
+ if (viewController != null) {
+ viewController.onSaveInstanceState(savedInstanceState);
+ }
+ savedInstanceState.putParcelable(VIEW_CONTROLLER_STATE_EXTRA, state);
+ }
+
+ /**
+ * Called when fragment is moved in stopped state or it's {@link #isMenuVisible()} sets to false.
+ * Usually it is indicating that user can't see fragment on screen and useful to track analytics events.
+ */
+ @CallSuper
+ protected void onDisappear() {
+ appeared = false;
+ if (viewController != null) {
+ viewController.onDisappear();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ if (appeared) {
+ onDisappear();
+ }
+ if (viewController != null) {
+ viewController.onStop();
+ }
+ super.onStop();
+ }
+
+ @Override
+ public void onDestroyView() {
+ if (viewController != null) {
+ viewController.onDestroy();
+ viewController = null;
+ }
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
+ if (viewController != null) {
+ viewController.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+
+ @Override
+ public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
+ if (viewController != null) {
+ viewController.onActivityResult(requestCode, resultCode, data);
+ } else {
+ pendingActivityResult = new ActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ @Override
+ public void setMenuVisibility(final boolean menuVisible) {
+ super.setMenuVisibility(menuVisible);
+ if (getActivity() != null && getView() != null) {
+ final boolean started = getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED);
+ if (!appeared && menuVisible && started) {
+ onAppear();
+ }
+ if (appeared && (!menuVisible || !started)) {
+ onDisappear();
+ }
+ }
+ }
+
+ /**
+ * Returns if fragment have parent fragment.
+ *
+ * @return Returns true if fragment is in some fragment's children stack.
+ */
+ public boolean isChildFragment() {
+ return getParentFragment() != null;
+ }
+
+ @NonNull
+ private ViewController createViewController(
+ @NonNull final FragmentActivity activity,
+ @NonNull final ViewGroup view,
+ @Nullable final Bundle savedInstanceState
+ ) {
+ if (viewControllerClass.getConstructors().length != 1) {
+ throw new ShouldNotHappenException("There should be single constructor for " + viewControllerClass);
+ }
+ final Constructor> constructor = viewControllerClass.getConstructors()[0];
+ final ViewController.CreationContext creationContext = new ViewController.CreationContext(activity, this, view);
+ final long creationTime = BuildConfig.DEBUG ? SystemClock.elapsedRealtime() : 0;
+ try {
+ switch (constructor.getParameterTypes().length) {
+ case 2:
+ return (ViewController) constructor.newInstance(creationContext, savedInstanceState);
+ case 3:
+ return (ViewController) constructor.newInstance(this, creationContext, savedInstanceState);
+ default:
+ throw new ShouldNotHappenException("Wrong constructor parameters count: " + constructor.getParameterTypes().length);
+ }
+ } catch (@NonNull final Exception exception) {
+ throw new ShouldNotHappenException(exception);
+ } finally {
+ checkCreationTime(creationTime);
+ }
+ }
+
+ private void checkCreationTime(final long creationTime) {
+ if (BuildConfig.DEBUG) {
+ final long creationPeriod = SystemClock.elapsedRealtime() - creationTime;
+ if (creationPeriod > acceptableUiCalculationTime) {
+ LcGroup.UI_METRICS.w("Creation of %s took too much: %dms", viewControllerClass, creationPeriod);
+ }
+ }
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return super.toString() + " ViewController: " + getViewControllerClass();
+ }
+
+ private static class PlaceholderView extends FrameLayout {
+
+ @NonNull
+ private final String tagName;
+ private long lastMeasureTime;
+
+ public PlaceholderView(@NonNull final Context context, @NonNull final String tagName) {
+ super(context);
+ this.tagName = tagName;
+ }
+
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ if (BuildConfig.DEBUG && lastMeasureTime == 0) {
+ lastMeasureTime = SystemClock.uptimeMillis();
+ }
+ }
+
+ @Override
+ protected void onDraw(@NonNull final Canvas canvas) {
+ super.onDraw(canvas);
+ if (BuildConfig.DEBUG && lastMeasureTime > 0) {
+ final long layoutTime = SystemClock.uptimeMillis() - lastMeasureTime;
+ if (layoutTime > acceptableUiCalculationTime) {
+ LcGroup.UI_METRICS.w("Measure and layout of %s took too much: %dms", tagName, layoutTime);
+ }
+ lastMeasureTime = 0;
+ }
+ }
+
+ }
+
+ private static class ActivityResult {
+ public final int requestCode;
+ public final int resultCode;
+ @Nullable
+ public final Intent data;
+
+ ActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
+ this.requestCode = requestCode;
+ this.resultCode = resultCode;
+ this.data = data;
+ }
+ }
+
+}
diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/DefaultViewController.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/DefaultViewController.kt
new file mode 100644
index 0000000..0fae24f
--- /dev/null
+++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/DefaultViewController.kt
@@ -0,0 +1,21 @@
+package ru.touchin.roboswag.components.navigation.viewcontrollers
+
+import android.os.Bundle
+import android.os.Parcelable
+import android.support.annotation.LayoutRes
+import android.support.v4.app.FragmentActivity
+
+abstract class DefaultViewController(
+ @LayoutRes layoutRes: Int,
+ creationContext: CreationContext,
+ savedInstanceState: Bundle?
+) : ViewController(
+ creationContext,
+ savedInstanceState
+) {
+
+ init {
+ setContentView(layoutRes)
+ }
+
+}
diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt
new file mode 100644
index 0000000..12424bc
--- /dev/null
+++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt
@@ -0,0 +1,19 @@
+package ru.touchin.roboswag.components.navigation.viewcontrollers
+
+import android.os.Parcel
+import android.os.Parcelable
+
+object EmptyState : Parcelable {
+
+ override fun writeToParcel(parcel: Parcel, flags: Int) = Unit
+
+ override fun describeContents() = 0
+
+ @JvmField
+ val CREATOR = object : Parcelable.Creator {
+ override fun createFromParcel(parcel: Parcel) = EmptyState
+
+ override fun newArray(size: Int): Array = arrayOfNulls(size)
+ }
+
+}
diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.java
new file mode 100644
index 0000000..0aa771b
--- /dev/null
+++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.java
@@ -0,0 +1,471 @@
+/*
+ * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.components.navigation.viewcontrollers;
+
+import android.animation.Animator;
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.LifecycleOwner;
+import android.arch.lifecycle.LifecycleRegistry;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.CallSuper;
+import android.support.annotation.ColorInt;
+import android.support.annotation.ColorRes;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.IdRes;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.content.ContextCompat;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+
+import ru.touchin.roboswag.components.navigation.fragments.ViewControllerFragment;
+import ru.touchin.roboswag.components.utils.UiUtils;
+import ru.touchin.roboswag.core.log.Lc;
+import ru.touchin.roboswag.core.log.LcGroup;
+
+/**
+ * Created by Gavriil Sitnikov on 21/10/2015.
+ * Class to control view of specific fragment, activity and application by logic bridge.
+ *
+ * @param Type of activity where such {@link ViewController} could be;
+ * @param Type of state;
+ */
+public class ViewController implements LifecycleOwner {
+
+ @NonNull
+ private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);
+ @NonNull
+ private final TActivity activity;
+ @NonNull
+ private final ViewControllerFragment fragment;
+ @NonNull
+ private final ViewGroup container;
+
+ @SuppressWarnings({"unchecked", "PMD.UnusedFormalParameter"})
+ //UnusedFormalParameter: savedInstanceState could be used by children
+ public ViewController(@NonNull final CreationContext creationContext, @Nullable final Bundle savedInstanceState) {
+ this.activity = (TActivity) creationContext.activity;
+ this.fragment = creationContext.fragment;
+ this.container = creationContext.container;
+ }
+
+ @NonNull
+ @Override
+ public Lifecycle getLifecycle() {
+ return lifecycleRegistry;
+ }
+
+ /**
+ * Returns activity where {@link ViewController} could be.
+ *
+ * @return Returns activity.
+ */
+ @NonNull
+ public final TActivity getActivity() {
+ return activity;
+ }
+
+ /**
+ * Returns fragment where {@link ViewController} could be.
+ *
+ * @return Returns fragment.
+ */
+ @NonNull
+ public final ViewControllerFragment getFragment() {
+ return fragment;
+ }
+
+ /**
+ * Returns state from fragment.
+ *
+ * @return Returns state.
+ */
+ @NonNull
+ protected final TState getState() {
+ return fragment.getState();
+ }
+
+ /**
+ * Returns view instantiated in {@link #getFragment()} fragment attached to {@link #getActivity()} activity.
+ * Use it to inflate your views into at construction of this {@link ViewController}.
+ *
+ * @return Returns view.
+ */
+ @NonNull
+ protected final ViewGroup getContainer() {
+ return container;
+ }
+
+ /**
+ * Set the view controller content from a layout resource.
+ * This layout is placed directly into the container's ({@link #getContainer()}) view hierarchy.
+ *
+ * @param layoutResId Resource ID to be inflated.
+ */
+ protected final void setContentView(@LayoutRes final int layoutResId) {
+ if (getContainer().getChildCount() > 0) {
+ getContainer().removeAllViews();
+ }
+ UiUtils.inflateAndAdd(layoutResId, getContainer());
+ }
+
+ /**
+ * Set the view controller content to an explicit view.
+ * This view is placed directly into the container's ({@link #getContainer()}) view hierarchy.
+ *
+ * @param view The desired content to display.
+ */
+ protected final void setContentView(@NonNull final View view) {
+ setContentView(view, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ }
+
+ /**
+ * Set the view controller content to an explicit view with specific layout parameters.
+ * This view is placed directly into the container's ({@link #getContainer()}) view hierarchy.
+ *
+ * @param view The desired content to display;
+ * @param layoutParams Layout parameters for the view.
+ */
+ protected final void setContentView(@NonNull final View view, @NonNull final ViewGroup.LayoutParams layoutParams) {
+ if (getContainer().getChildCount() > 0) {
+ getContainer().removeAllViews();
+ }
+ getContainer().addView(view, layoutParams);
+ }
+
+ /**
+ * Look for a child view with the given id. If this view has the given id, return this view.
+ *
+ * @param id The id to search for;
+ * @return The view that has the given id in the hierarchy.
+ */
+ @NonNull
+ public final T findViewById(@IdRes final int id) {
+ return getContainer().findViewById(id);
+ }
+
+ /**
+ * Return a localized, styled CharSequence from the application's package's
+ * default string table.
+ *
+ * @param resId Resource id for the CharSequence text
+ */
+ @NonNull
+ public final CharSequence getText(@StringRes final int resId) {
+ return activity.getText(resId);
+ }
+
+ /**
+ * Return a localized string from the application's package's default string table.
+ *
+ * @param resId Resource id for the string
+ */
+ @NonNull
+ public final String getString(@StringRes final int resId) {
+ return activity.getString(resId);
+ }
+
+ /**
+ * Return a localized formatted string from the application's package's default string table, substituting the format arguments as defined in
+ * {@link java.util.Formatter} and {@link java.lang.String#format}.
+ *
+ * @param resId Resource id for the format string
+ * @param formatArgs The format arguments that will be used for substitution.
+ */
+ @NonNull
+ public final String getString(@StringRes final int resId, @NonNull final Object... formatArgs) {
+ return activity.getString(resId, formatArgs);
+ }
+
+ /**
+ * Return the color value associated with a particular resource ID.
+ * Starting in {@link android.os.Build.VERSION_CODES#M}, the returned
+ * color will be styled for the specified Context's theme.
+ *
+ * @param resId The resource id to search for data;
+ * @return int A single color value in the form 0xAARRGGBB.
+ */
+ @ColorInt
+ public final int getColor(@ColorRes final int resId) {
+ return ContextCompat.getColor(activity, resId);
+ }
+
+ /**
+ * Returns a color state list associated with a particular resource ID.
+ *
+ *
Starting in {@link android.os.Build.VERSION_CODES#M}, the returned
+ * color state list will be styled for the specified Context's theme.
+ *
+ * @param resId The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ * @return A color state list, or {@code null} if the resource could not be resolved.
+ * @throws android.content.res.Resources.NotFoundException if the given ID
+ * does not exist.
+ */
+ @Nullable
+ public final ColorStateList getColorStateList(@ColorRes final int resId) {
+ return ContextCompat.getColorStateList(activity, resId);
+ }
+
+ /**
+ * Returns a drawable object associated with a particular resource ID.
+ * Starting in {@link android.os.Build.VERSION_CODES#LOLLIPOP}, the
+ * returned drawable will be styled for the specified Context's theme.
+ *
+ * @param resId The resource id to search for data;
+ * @return Drawable An object that can be used to draw this resource.
+ */
+ @Nullable
+ public final Drawable getDrawable(@DrawableRes final int resId) {
+ return ContextCompat.getDrawable(activity, resId);
+ }
+
+ public final void startActivity(@NonNull final Intent intent) {
+ fragment.startActivity(intent);
+ }
+
+ public final void startActivityForResult(@NonNull final Intent intent, final int requestCode) {
+ fragment.startActivityForResult(intent, requestCode);
+ }
+
+ /**
+ * Calls when activity configuring ActionBar, Toolbar, Sidebar etc.
+ * If it will be called or not depends on {@link Fragment#hasOptionsMenu()} and {@link Fragment#isMenuVisible()}.
+ *
+ * @param menu The options menu in which you place your items;
+ * @param inflater Helper to inflate menu items.
+ */
+ public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) {
+ // do nothing
+ }
+
+ /**
+ * Calls right after construction of {@link ViewController}.
+ * Happens at {@link ViewControllerFragment#onActivityCreated(Bundle)}.
+ */
+ @CallSuper
+ public void onCreate() {
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
+ }
+
+ /**
+ * Called when a fragment loads an animation. Note that if
+ * {@link FragmentTransaction#setCustomAnimations(int, int)} was called with
+ * {@link Animator} resources instead of {@link Animation} resources, {@code nextAnim}
+ * will be an animator resource.
+ *
+ * @param transit The value set in {@link FragmentTransaction#setTransition(int)} or 0 if not
+ * set.
+ * @param enter {@code true} when the fragment is added/attached/shown or {@code false} when
+ * the fragment is removed/detached/hidden.
+ * @param nextAnim The resource set in
+ * {@link FragmentTransaction#setCustomAnimations(int, int)},
+ * {@link FragmentTransaction#setCustomAnimations(int, int, int, int)}, or
+ * 0 if neither was called. The value will depend on the current operation.
+ */
+ @Nullable
+ public Animation onCreateAnimation(final int transit, final boolean enter, final int nextAnim) {
+ return null;
+ }
+
+ /**
+ * Called when a fragment loads an animator. This will be called when
+ * {@link #onCreateAnimation(int, boolean, int)} returns null. Note that if
+ * {@link FragmentTransaction#setCustomAnimations(int, int)} was called with
+ * {@link Animation} resources instead of {@link Animator} resources, {@code nextAnim}
+ * will be an animation resource.
+ *
+ * @param transit The value set in {@link FragmentTransaction#setTransition(int)} or 0 if not
+ * set.
+ * @param enter {@code true} when the fragment is added/attached/shown or {@code false} when
+ * the fragment is removed/detached/hidden.
+ * @param nextAnim The resource set in
+ * {@link FragmentTransaction#setCustomAnimations(int, int)},
+ * {@link FragmentTransaction#setCustomAnimations(int, int, int, int)}, or
+ * 0 if neither was called. The value will depend on the current operation.
+ */
+ @Nullable
+ public Animator onCreateAnimator(final int transit, final boolean enter, final int nextAnim) {
+ return null;
+ }
+
+ /**
+ * Calls when {@link ViewController} saved state has been restored into the view hierarchy.
+ * Happens at {@link ViewControllerFragment#onViewStateRestored}.
+ */
+ @CallSuper
+ public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
+ // do nothing
+ }
+
+ /**
+ * Calls when {@link ViewController} have started.
+ * Happens at {@link ViewControllerFragment#onStart()}.
+ */
+ @CallSuper
+ public void onStart() {
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ UiUtils.OfViews.hideSoftInput(getContainer());
+ }
+
+ /**
+ * Called when fragment is moved in started state and it's {@link #getFragment().isMenuVisible()} sets to true.
+ * Usually it is indicating that user can't see fragment on screen and useful to track analytics events.
+ */
+ public void onAppear() {
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ }
+
+ /**
+ * Calls when {@link ViewController} have resumed.
+ * Happens at {@link ViewControllerFragment#onResume()}.
+ */
+ @CallSuper
+ public void onResume() {
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
+ }
+
+ /**
+ * Calls when {@link ViewController} have goes near out of memory state.
+ * Happens at {@link ViewControllerFragment#onLowMemory()}.
+ */
+ @CallSuper
+ public void onLowMemory() {
+ //do nothing
+ }
+
+ /**
+ * Calls when {@link ViewController} have paused.
+ * Happens at {@link ViewControllerFragment#onPause()}.
+ */
+ @CallSuper
+ public void onPause() {
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
+ }
+
+ /**
+ * Calls when {@link ViewController} should save it's state.
+ * Happens at {@link ViewControllerFragment#onSaveInstanceState(Bundle)}.
+ * Try not to use such method for saving state but use {@link ViewControllerFragment#getState()} from {@link #getFragment()}.
+ */
+ @CallSuper
+ public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) {
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ }
+
+ /**
+ * Called when fragment is moved in stopped state or it's {@link #getFragment().isMenuVisible()} sets to false.
+ * Usually it is indicating that user can't see fragment on screen and useful to track analytics events.
+ */
+ public void onDisappear() {
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ }
+
+ /**
+ * Calls when {@link ViewController} have stopped.
+ * Happens at {@link ViewControllerFragment#onStop()}.
+ */
+ @CallSuper
+ public void onStop() {
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
+ }
+
+ /**
+ * Calls when {@link ViewController} have destroyed.
+ * Happens usually at {@link ViewControllerFragment#onDestroyView()}. In some cases at {@link ViewControllerFragment#onDestroy()}.
+ */
+ @CallSuper
+ public void onDestroy() {
+ LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+ }
+
+
+ /**
+ * Calls when {@link ViewController} have requested permissions results.
+ * Happens at {@link ViewControllerFragment#onRequestPermissionsResult(int, String[], int[])} ()}.
+ */
+ @CallSuper
+ @SuppressWarnings("PMD.UseVarargs")
+ public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
+ //do nothing
+ }
+
+ /**
+ * Callback from parent fragment.
+ */
+ public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
+ // do nothing
+ }
+
+ /**
+ * Similar to {@link ViewControllerFragment#onOptionsItemSelected(MenuItem)}.
+ *
+ * @param item Selected menu item;
+ * @return True if selection processed.
+ */
+ public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
+ return false;
+ }
+
+ /*
+ * Helper class to simplify constructor override.
+ */
+ public static class CreationContext {
+
+ @NonNull
+ private final FragmentActivity activity;
+ @NonNull
+ private final ViewControllerFragment fragment;
+ @NonNull
+ private final ViewGroup container;
+
+ public CreationContext(
+ @NonNull final FragmentActivity activity,
+ @NonNull final ViewControllerFragment fragment,
+ @NonNull final ViewGroup container
+ ) {
+ this.activity = activity;
+ this.fragment = fragment;
+ this.container = container;
+ }
+
+ }
+
+}
diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewControllerNavigation.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewControllerNavigation.kt
new file mode 100644
index 0000000..a8e4b87
--- /dev/null
+++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewControllerNavigation.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.components.navigation.viewcontrollers
+
+import android.content.Context
+import android.os.Parcelable
+import android.support.annotation.IdRes
+import android.support.v4.app.Fragment
+import android.support.v4.app.FragmentActivity
+import android.support.v4.app.FragmentManager
+import android.support.v4.app.FragmentTransaction
+
+import ru.touchin.roboswag.components.navigation.FragmentNavigation
+import ru.touchin.roboswag.components.navigation.fragments.ViewControllerFragment
+
+/**
+ * Created by Gavriil Sitnikov on 07/03/2016.
+ * Navigation based on [ViewController]s which are creating by [Fragment]s.
+ * So basically it is just [FragmentNavigation] where most of fragments should be inherited from [ViewControllerFragment].
+ *
+ * @param TActivity Type of activity where [ViewController]s should be showed.
+ */
+open class ViewControllerNavigation(
+ context: Context,
+ fragmentManager: FragmentManager,
+ @IdRes containerViewId: Int,
+ transition: Int = FragmentTransaction.TRANSIT_FRAGMENT_OPEN
+) : FragmentNavigation(context, fragmentManager, containerViewId, transition) {
+
+ /**
+ * Pushes [ViewController] on top of stack with specific [ViewControllerFragment.getState] and with specific transaction setup.
+ *
+ * @param viewControllerClass Class of [ViewController] to be pushed;
+ * @param state [Parcelable] of [ViewController]'s fragment;
+ * @param addToStack Flag to add this transaction to the back stack;
+ * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info;
+ * @param TState Type of state of fragment.
+ */
+ fun pushViewController(
+ viewControllerClass: Class>,
+ state: TState,
+ addToStack: Boolean = true,
+ transactionSetup: ((FragmentTransaction) -> Unit)? = null
+ ) {
+ addToStack(
+ ViewControllerFragment::class.java,
+ null,
+ 0,
+ addToStack,
+ ViewControllerFragment.args(viewControllerClass, state),
+ null,
+ transactionSetup
+ )
+ }
+
+ /**
+ * Pushes [ViewController] on top of stack with specific [ViewControllerFragment.getState]
+ * and with specific [TTargetFragment] and transaction setup.
+ *
+ * @param viewControllerClass Class of [ViewController] to be pushed;
+ * @param targetFragment [ViewControllerFragment] to be set as target;
+ * @param state [Parcelable] of [ViewController]'s fragment;
+ * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info;
+ * @param TState Type of state of fragment;
+ * @param TTargetFragment Type of target fragment.
+ */
+ fun pushViewControllerForResult(
+ viewControllerClass: Class>,
+ state: TState,
+ targetFragment: TTargetFragment,
+ targetRequestCode: Int,
+ transactionSetup: ((FragmentTransaction) -> Unit)? = null
+ ) {
+ addToStack(
+ ViewControllerFragment::class.java,
+ targetFragment,
+ targetRequestCode,
+ true,
+ ViewControllerFragment.args(viewControllerClass, state),
+ null,
+ transactionSetup
+ )
+ }
+
+ /**
+ * Pushes [ViewController] on top of stack with specific [ViewControllerFragment.getState] and with specific transaction setup
+ * and with [.TOP_FRAGMENT_TAG_MARK] tag used for simple up/back navigation.
+ *
+ * @param viewControllerClass Class of [ViewController] to be pushed;
+ * @param state [Parcelable] of [ViewController]'s fragment;
+ * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info;
+ * @param TState Type of state of fragment.
+ */
+ fun setViewControllerAsTop(
+ viewControllerClass: Class>,
+ state: TState,
+ addToStack: Boolean = true,
+ transactionSetup: ((FragmentTransaction) -> Unit)? = null
+ ) {
+ addToStack(
+ ViewControllerFragment::class.java,
+ null,
+ 0,
+ addToStack,
+ ViewControllerFragment.args(viewControllerClass, state),
+ "${viewControllerClass.name};$TOP_FRAGMENT_TAG_MARK",
+ transactionSetup
+ )
+ }
+
+ /**
+ * Pops all [Fragment]s and places new initial [ViewController] on top of stack
+ * with specific [ViewControllerFragment.getState] and specific transaction setup.
+ *
+ * @param viewControllerClass Class of [ViewController] to be pushed;
+ * @param state [Parcelable] of [ViewController]'s fragment;
+ * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info;
+ * @param TState Type of state of fragment.
+ */
+ fun setInitialViewController(
+ viewControllerClass: Class>,
+ state: TState,
+ transactionSetup: ((FragmentTransaction) -> Unit)? = null
+ ) {
+ beforeSetInitialActions()
+ setViewControllerAsTop(viewControllerClass, state, false, transactionSetup)
+ }
+
+}
diff --git a/recyclerview-adapters/.gitignore b/recyclerview-adapters/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/recyclerview-adapters/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/recyclerview-adapters/build.gradle b/recyclerview-adapters/build.gradle
new file mode 100644
index 0000000..12fc082
--- /dev/null
+++ b/recyclerview-adapters/build.gradle
@@ -0,0 +1,18 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion versions.compileSdk
+
+ defaultConfig {
+ minSdkVersion versions.minSdk
+ }
+}
+
+dependencies {
+ api project(':kotlin-extensions')
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+
+ implementation "com.android.support:recyclerview-v7:$versions.supportLibrary"
+}
diff --git a/recyclerview-adapters/src/main/AndroidManifest.xml b/recyclerview-adapters/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..2228245
--- /dev/null
+++ b/recyclerview-adapters/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
diff --git a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java
new file mode 100644
index 0000000..25be0c9
--- /dev/null
+++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.components.adapters;
+
+import android.support.annotation.NonNull;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.ViewGroup;
+
+import java.util.List;
+
+/**
+ * Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders.
+ * Default {@link #getItemViewType} is generating on construction of object.
+ *
+ * @param Type of {@link RecyclerView.ViewHolder} of delegate.
+ */
+public abstract class AdapterDelegate {
+
+ private final int defaultItemViewType = ViewCompat.generateViewId();
+
+ /**
+ * Unique ID of AdapterDelegate.
+ *
+ * @return Unique ID.
+ */
+ public int getItemViewType() {
+ return defaultItemViewType;
+ }
+
+ /**
+ * Returns if object is processable by this delegate.
+ *
+ * @param items Items to check;
+ * @param adapterPosition Position of item in adapter;
+ * @param collectionPosition Position of item in collection;
+ * @return True if item is processable by this delegate.
+ */
+ public abstract boolean isForViewType(@NonNull final List