This commit is contained in:
Denis Karmyshakov 2018-08-22 15:22:12 +00:00 committed by GitHub
commit de32f48c57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
189 changed files with 7673 additions and 7029 deletions

36
.gitignore vendored
View File

@ -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

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "BuildScripts"]
path = BuildScripts
url = git@github.com:TouchInstinct/BuildScripts.git

1
BuildScripts Submodule

@ -0,0 +1 @@
Subproject commit 3fe4b2a1aea149050b34c60ec7e2a876dfd8c0b2

1
api-logansquare/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -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'
}

View File

@ -0,0 +1,2 @@
<manifest
package="ru.touchin.templates.logansquare"/>

View File

@ -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() {
}
}

View File

@ -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);
}
}
}

View File

@ -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<BigDecimal> {
@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);
}
}
}

View File

@ -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();
}

View File

@ -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<T extends Enum & LoganSquareEnum> extends StringBasedTypeConverter<T> {
@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();
}
}

View File

@ -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<DateTime> {
@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);
}
}
}

View File

@ -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<ResponseBody, ?> 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<T> extends JsonResponseBodyConverter<T> {
@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<T> extends JsonRequestBodyConverter<T> {
@Override
protected void writeValueToByteArray(@NonNull final T value, @NonNull final ByteArrayOutputStream byteArrayOutputStream) throws IOException {
LoganSquare.serialize(value, byteArrayOutputStream);
}
}
public static class LoganSquareStringEnumConverter<T> implements Converter<T, String> {
@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<T>) value.getClass()).serialize(value, null, false, generator);
generator.close();
return writer.toString().replaceAll("\"", "");
} finally {
writer.close();
}
}
}
}

View File

@ -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));
}
}
}

View File

@ -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 <T> Storable<String, T, String> jsonStorable(@NonNull final String name,
@NonNull final Class<T> jsonClass,
@NonNull final SharedPreferences preferences) {
return new Storable.Builder<String, T, String>(name, jsonClass, String.class, new PreferenceStore<>(preferences), new JsonConverter<>())
.setObserveStrategy(Storable.ObserveStrategy.CACHE_ACTUAL_VALUE)
.build();
}
@NonNull
public static <T> NonNullStorable<String, T, String> jsonStorable(@NonNull final String name,
@NonNull final Class<T> jsonClass,
@NonNull final SharedPreferences preferences,
@NonNull final T defaultValue) {
return new Storable.Builder<String, T, String>(name, jsonClass, String.class, new PreferenceStore<>(preferences), new JsonConverter<>())
.setObserveStrategy(Storable.ObserveStrategy.CACHE_ACTUAL_VALUE)
.setDefaultValue(defaultValue)
.build();
}
@NonNull
public static <T> Storable<String, List<T>, String> jsonListStorable(@NonNull final String name,
@NonNull final Class<T> 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 <T> NonNullStorable<String, List<T>, String> jsonListStorable(@NonNull final String name,
@NonNull final Class<T> jsonListItemClass,
@NonNull final SharedPreferences preferences,
@NonNull final List<T> 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<TJsonObject> implements Converter<TJsonObject, String> {
@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<TJsonObject>) jsonObjectClass);
} catch (final IOException exception) {
throw new ShouldNotHappenException(exception);
}
}
}
public static class JsonListConverter<T> implements Converter<List<T>, String> {
@NonNull
private final Class<T> itemClass;
public JsonListConverter(@NonNull final Class<T> itemClass) {
this.itemClass = itemClass;
}
@Nullable
@Override
@SuppressWarnings("unchecked")
public String toStoreObject(@NonNull final Type jsonObjectType, @NonNull final Type stringType, @Nullable final List<T> object) {
if (object == null) {
return null;
}
try {
return LoganSquare.serialize(object, itemClass);
} catch (final IOException exception) {
throw new ShouldNotHappenException(exception);
}
}
@Nullable
@Override
public List<T> 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);
}
}
}
}

View File

@ -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 <T> Type of body object.
*/
public abstract class JsonRequestBodyConverter<T> implements Converter<T, RequestBody> {
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;
}

View File

@ -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 <T> Type of response object.
*/
public abstract class JsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
@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;
}

View File

@ -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'
]
}

13
gradle.properties Normal file
View File

@ -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

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -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

172
gradlew vendored Executable file
View File

@ -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" "$@"

84
gradlew.bat vendored Normal file
View File

@ -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

1
kotlin-extensions/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -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"
}

View File

@ -0,0 +1,2 @@
<manifest
package="ru.touchin.roboswag.components.extensions"/>

View File

@ -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 <T> Delegates.observable(
initialValue: T,
crossinline onChange: (newValue: T) -> Unit
): ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(newValue)
}
inline fun <T> Delegates.distinctUntilChanged(
initialValue: T,
crossinline onChange: (newValue: T) -> Unit
): ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) =
if (newValue != null && oldValue != newValue) onChange(newValue) else Unit
}

View File

@ -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)
}
}

View File

@ -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 <T : View> 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)

1
lifecycle-common/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -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"
}

View File

@ -0,0 +1 @@
<manifest package="ru.touchin.lifecycle.common"/>

View File

@ -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<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<T?>) {
super.observe(owner, Observer { value ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(value)
}
})
}
override fun setValue(value: T) {
pending.set(true)
super.setValue(value)
}
}

View File

@ -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}.
* <p>
* 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.")
}
}

View File

@ -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<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T
= (creators[modelClass]?.get() as? T) ?: throw IllegalArgumentException("Unknown model class $modelClass")
}

View File

@ -0,0 +1,7 @@
package ru.touchin.templates.viewmodel
interface ViewModelFactoryProvider {
val viewModelFactory: ViewModelFactory
}

1
lifecycle-rx/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

24
lifecycle-rx/build.gradle Normal file
View File

@ -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"
}

View File

@ -0,0 +1 @@
<manifest package="ru.touchin.lifecycle.rx"/>

View File

@ -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 <T> Flowable<T>.untilDestroy(
onNext: (T) -> Unit,
onError: (Throwable) -> Unit,
onComplete: () -> Unit
): Disposable = observeOn(AndroidSchedulers.mainThread())
.subscribe(onNext, onError, onComplete)
.also { subscriptions.add(it) }
override fun <T> Observable<T>.untilDestroy(
onNext: (T) -> Unit,
onError: (Throwable) -> Unit,
onComplete: () -> Unit
): Disposable = observeOn(AndroidSchedulers.mainThread())
.subscribe(onNext, onError, onComplete)
.also { subscriptions.add(it) }
override fun <T> Single<T>.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 <T> Maybe<T>.untilDestroy(
onSuccess: (T) -> Unit,
onError: (Throwable) -> Unit,
onComplete: () -> Unit
): Disposable = observeOn(AndroidSchedulers.mainThread())
.subscribe(onSuccess, onError, onComplete)
.also { subscriptions.add(it) }
}

View File

@ -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 <T> Flowable<T>.untilDestroy(
onNext: (T) -> Unit = Functions.emptyConsumer<T>()::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 <T> Observable<T>.untilDestroy(
onNext: (T) -> Unit = Functions.emptyConsumer<T>()::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 <T> Single<T>.untilDestroy(
onSuccess: (T) -> Unit = Functions.emptyConsumer<T>()::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 <T> Maybe<T>.untilDestroy(
onSuccess: (T) -> Unit = Functions.emptyConsumer<T>()::accept,
onError: (Throwable) -> Unit = getActionThrowableForAssertion(Lc.getCodePoint(this, 2)),
onComplete: () -> Unit = Functions.EMPTY_ACTION::run
): Disposable
}

View File

@ -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 <T> Flowable<out T>.dispatchTo(liveData: MutableLiveData<ObservableEvent<T>>): 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 <T> Observable<out T>.dispatchTo(liveData: MutableLiveData<ObservableEvent<T>>): 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 <T> Single<out T>.dispatchTo(liveData: MutableLiveData<SingleEvent<T>>): 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<CompletableEvent>): Disposable {
liveData.value = CompletableEvent.Loading
return untilDestroy(
{ liveData.value = CompletableEvent.Completed },
{ throwable -> liveData.value = CompletableEvent.Error(throwable) })
}
override fun <T> Maybe<out T>.dispatchTo(liveData: MutableLiveData<MaybeEvent<T>>): 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) })
}
}

View File

@ -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 <T> Flowable<out T>.dispatchTo(liveData: MutableLiveData<ObservableEvent<T>>): Disposable
fun <T> Observable<out T>.dispatchTo(liveData: MutableLiveData<ObservableEvent<T>>): Disposable
fun <T> Single<out T>.dispatchTo(liveData: MutableLiveData<SingleEvent<T>>): Disposable
fun Completable.dispatchTo(liveData: MutableLiveData<CompletableEvent>): Disposable
fun <T> Maybe<out T>.dispatchTo(liveData: MutableLiveData<MaybeEvent<T>>): Disposable
}

View File

@ -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()
}

View File

@ -0,0 +1,16 @@
package ru.touchin.templates.livedata.event
/**
* Event class that emits from [io.reactivex.Maybe].
*/
sealed class MaybeEvent<out T>(open val data: T?) {
class Loading<out T>(data: T?): MaybeEvent<T>(data)
class Success<out T>(override val data: T): MaybeEvent<T>(data)
class Error<out T>(val throwable: Throwable, data: T?): MaybeEvent<T>(data)
class Completed<out T>(data: T?): MaybeEvent<T>(data)
}

View File

@ -0,0 +1,16 @@
package ru.touchin.templates.livedata.event
/**
* Event class that emits from [io.reactivex.Observable].
*/
sealed class ObservableEvent<out T>(open val data: T?) {
class Loading<out T>(data: T? = null): ObservableEvent<T>(data)
class Success<out T>(override val data: T): ObservableEvent<T>(data)
class Error<out T>(val throwable: Throwable, data: T? = null): ObservableEvent<T>(data)
class Completed<out T>(data: T? = null): ObservableEvent<T>(data)
}

View File

@ -0,0 +1,14 @@
package ru.touchin.templates.livedata.event
/**
* Event class that emits from [io.reactivex.Single].
*/
sealed class SingleEvent<out T>(open val data: T?) {
class Loading<out T>(data: T?): SingleEvent<T>(data)
class Success<out T>(override val data: T): SingleEvent<T>(data)
class Error<out T>(val throwable: Throwable, data: T?): SingleEvent<T>(data)
}

View File

@ -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()
}
}

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">lifecycle-rx</string>
</resources>

1
logging/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

18
logging/build.gradle Normal file
View File

@ -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"
}

View File

@ -0,0 +1 @@
<manifest package="ru.touchin.roboswag.core.log" />

View File

@ -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);
}
}
}

View File

@ -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<Throwable> 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() {
}
}

View File

@ -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<SimpleDateFormat> 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));
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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<T> extends ThreadLocal<T> {
@NonNull
private final Fabric<T> fabric;
public ThreadLocalValue(@NonNull final Fabric<T> 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 <T> Type of objects.
*/
void onFragmentStarted(@NonNull Fragment fragment);
public interface Fabric<T> {
}
/**
* Creates object.
*
* @return new instance of object.
*/
@NonNull
T create();
}
}

30
modules.gradle Normal file
View File

@ -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')

1
navigation/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

24
navigation/build.gradle Normal file
View File

@ -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"
}

View File

@ -0,0 +1,3 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="ru.touchin.roboswag.components.navigation"/>

View File

@ -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<out Fragment>,
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<out Fragment>,
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<out Fragment>,
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<out Fragment>,
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<out Fragment>,
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)
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<OnBackPressedListener> 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();
}
}

View File

@ -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();
}

View File

@ -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 <TState> Type of object which is representing it's fragment state;
* @param <TActivity> Type of {@link FragmentActivity} where fragment could be attached to.
*/
@SuppressWarnings("PMD.TooManyMethods")
public class ViewControllerFragment<TActivity extends FragmentActivity, TState extends Parcelable> 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 extends Parcelable> 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<ViewController<TActivity, TState>> 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<ViewController<TActivity, TState>> getViewControllerClass() {
return viewControllerClass;
}
@SuppressWarnings("unchecked")
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(!isChildFragment());
viewControllerClass = (Class<ViewController<TActivity, TState>>) 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;
}
}
}

View File

@ -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<TActivity : FragmentActivity, TState : Parcelable>(
@LayoutRes layoutRes: Int,
creationContext: CreationContext,
savedInstanceState: Bundle?
) : ViewController<TActivity, TState>(
creationContext,
savedInstanceState
) {
init {
setContentView(layoutRes)
}
}

View File

@ -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<EmptyState> {
override fun createFromParcel(parcel: Parcel) = EmptyState
override fun newArray(size: Int): Array<EmptyState?> = arrayOfNulls(size)
}
}

View File

@ -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 <TActivity> Type of activity where such {@link ViewController} could be;
* @param <TState> Type of state;
*/
public class ViewController<TActivity extends FragmentActivity, TState extends Parcelable> implements LifecycleOwner {
@NonNull
private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);
@NonNull
private final TActivity activity;
@NonNull
private final ViewControllerFragment<TActivity, TState> 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<TActivity, TState> 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 extends View> 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.
*
* <p>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;
}
}
}

View File

@ -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<TActivity : FragmentActivity>(
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 <TState : Parcelable> pushViewController(
viewControllerClass: Class<out ViewController<out TActivity, TState>>,
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 <TState : Parcelable, TTargetFragment : Fragment> pushViewControllerForResult(
viewControllerClass: Class<out ViewController<out TActivity, TState>>,
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 <TState : Parcelable> setViewControllerAsTop(
viewControllerClass: Class<out ViewController<out TActivity, TState>>,
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 <TState : Parcelable> setInitialViewController(
viewControllerClass: Class<out ViewController<out TActivity, TState>>,
state: TState,
transactionSetup: ((FragmentTransaction) -> Unit)? = null
) {
beforeSetInitialActions()
setViewControllerAsTop(viewControllerClass, state, false, transactionSetup)
}
}

1
recyclerview-adapters/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -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"
}

View File

@ -0,0 +1,2 @@
<manifest
package="ru.touchin.roboswag.components.adapters"/>

View File

@ -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 <TViewHolder> Type of {@link RecyclerView.ViewHolder} of delegate.
*/
public abstract class AdapterDelegate<TViewHolder extends RecyclerView.ViewHolder> {
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<Object> items, final int adapterPosition, final int collectionPosition);
/**
* Returns unique ID of item to support stable ID's logic of RecyclerView's adapter.
*
* @param items Items in adapter;
* @param adapterPosition Position of item in adapter;
* @param collectionPosition Position of item in collection;
* @return Unique item ID.
*/
public long getItemId(@NonNull final List<Object> items, final int adapterPosition, final int collectionPosition) {
return 0;
}
/**
* Creates ViewHolder to bind item to it later.
*
* @param parent Container of ViewHolder's view.
* @return New ViewHolder.
*/
@NonNull
public abstract TViewHolder onCreateViewHolder(@NonNull final ViewGroup parent);
/**
* Binds item to created by this object ViewHolder.
*
* @param holder ViewHolder to bind item to;
* @param items Items in adapter;
* @param adapterPosition Position of item in adapter;
* @param collectionPosition Position of item in collection that contains item;
* @param payloads Payloads;
*/
public abstract void onBindViewHolder(
@NonNull final RecyclerView.ViewHolder holder,
@NonNull final List<Object> items,
final int adapterPosition,
final int collectionPosition,
@NonNull final List<Object> payloads
);
}

View File

@ -0,0 +1,52 @@
package ru.touchin.roboswag.components.adapters
import android.support.v7.widget.RecyclerView
import android.util.SparseArray
import android.view.ViewGroup
/**
* Manager for delegation callbacks from [RecyclerView.Adapter] to delegates.
*/
class DelegatesManager {
private val delegates = SparseArray<AdapterDelegate<*>>()
fun getItemViewType(items: List<*>, adapterPosition: Int, collectionPosition: Int): Int {
for (index in 0 until delegates.size()) {
val delegate = delegates.valueAt(index)
if (delegate.isForViewType(items, adapterPosition, collectionPosition)) {
return delegate.itemViewType
}
}
throw IllegalStateException("Delegate not found for adapterPosition: $adapterPosition")
}
fun getItemId(items: List<*>, adapterPosition: Int, collectionPosition: Int): Long {
val delegate = getDelegate(getItemViewType(items, adapterPosition, collectionPosition))
return delegate.getItemId(items, adapterPosition, collectionPosition)
}
fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = getDelegate(viewType).onCreateViewHolder(parent)
fun onBindViewHolder(holder: RecyclerView.ViewHolder, items: List<*>, adapterPosition: Int, collectionPosition: Int, payloads: List<Any>) {
val delegate = getDelegate(getItemViewType(items, adapterPosition, collectionPosition))
delegate.onBindViewHolder(holder, items, adapterPosition, collectionPosition, payloads)
}
/**
* Adds [PositionAdapterDelegate] to adapter.
*
* @param delegate Delegate to add.
*/
fun addDelegate(delegate: AdapterDelegate<*>) = delegates.put(delegate.itemViewType, delegate)
/**
* Removes [AdapterDelegate] from adapter.
*
* @param delegate Delegate to remove.
*/
fun removeDelegate(delegate: AdapterDelegate<*>) = delegates.remove(delegate.itemViewType)
private fun getDelegate(viewType: Int) = delegates[viewType] ?: throw IllegalStateException("No AdapterDelegate added for view type: $viewType")
}

View File

@ -0,0 +1,89 @@
package ru.touchin.roboswag.components.adapters
import android.support.v7.recyclerview.extensions.AsyncDifferConfig
import android.support.v7.recyclerview.extensions.AsyncListDiffer
import android.support.v7.util.DiffUtil
import android.support.v7.widget.RecyclerView
import android.view.ViewGroup
import ru.touchin.roboswag.components.extensions.setOnRippleClickListener
/**
* Base adapter with delegation and diff computing on background thread.
*/
open class DelegationListAdapter<TItem>(config: AsyncDifferConfig<TItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
constructor(diffCallback: DiffUtil.ItemCallback<TItem>) : this(AsyncDifferConfig.Builder<TItem>(diffCallback).build())
var itemClickListener: ((TItem, RecyclerView.ViewHolder) -> Unit)? = null
private val delegatesManager = DelegatesManager()
private var differ = AsyncListDiffer(OffsetAdapterUpdateCallback(this, ::getHeadersCount), config)
open fun getHeadersCount() = 0
open fun getFootersCount() = 0
override fun getItemCount() = getHeadersCount() + getList().size + getFootersCount()
override fun getItemViewType(position: Int) = delegatesManager.getItemViewType(getList(), position, getCollectionPosition(position))
override fun getItemId(position: Int) = delegatesManager.getItemId(getList(), position, getCollectionPosition(position))
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = delegatesManager.onCreateViewHolder(parent, viewType)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any>) {
val collectionPosition = getCollectionPosition(position)
if (collectionPosition in 0 until getList().size) {
if (itemClickListener != null) {
holder.itemView.setOnRippleClickListener {
itemClickListener?.invoke(getList()[getCollectionPosition(holder.adapterPosition)], holder)
}
} else {
holder.itemView.setOnClickListener(null)
}
}
delegatesManager.onBindViewHolder(holder, getList(), position, collectionPosition, payloads)
}
final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) = Unit
/**
* Adds [AdapterDelegate] to adapter.
*
* @param delegate Delegate to add.
*/
fun addDelegate(delegate: AdapterDelegate<*>) = delegatesManager.addDelegate(delegate)
/**
* Removes [AdapterDelegate] from adapter.
*
* @param delegate Delegate to remove.
*/
fun removeDelegate(delegate: AdapterDelegate<*>) = delegatesManager.removeDelegate(delegate)
/**
* Submits a new list to be diffed, and displayed.
*
* If a list is already being displayed, a diff will be computed on a background thread, which
* will dispatch Adapter.notifyItem events on the main thread.
*
* @param list The new list to be displayed.
*/
fun submitList(list: List<TItem>) = differ.submitList(list)
/**
* Get the current List - any diffing to present this list has already been computed and
* dispatched via the ListUpdateCallback.
* <p>
* If a <code>null</code> List, or no List has been submitted, an empty list will be returned.
* <p>
* The returned list may not be mutated - mutations to content must be done through
* {@link #submitList(List)}.
*
* @return current List.
*/
fun getList(): List<TItem> = differ.currentList
fun getCollectionPosition(adapterPosition: Int) = adapterPosition - getHeadersCount()
}

View File

@ -0,0 +1,85 @@
package ru.touchin.roboswag.components.adapters;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import java.util.List;
/**
* Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders.
* Such delegates are creating and binding ViewHolders for specific items.
* Default {@link #getItemViewType} is generating on construction of object.
*
* @param <TViewHolder> Type of {@link RecyclerView.ViewHolder} of delegate;
* @param <TItem> Type of items to bind to {@link RecyclerView.ViewHolder}s.
*/
public abstract class ItemAdapterDelegate<TViewHolder extends RecyclerView.ViewHolder, TItem> extends AdapterDelegate<TViewHolder> {
@Override
public boolean isForViewType(@NonNull final List<Object> items, final int adapterPosition, final int collectionPosition) {
return collectionPosition >= 0
&& collectionPosition < items.size()
&& isForViewType(items.get(collectionPosition), adapterPosition, collectionPosition);
}
/**
* Returns if object is processable by this delegate.
* This item will be casted to {@link TItem} and passes to {@link #onBindViewHolder(TViewHolder, TItem, int, int, List)}.
*
* @param item Item to check;
* @param adapterPosition Position of item in adapter;
* @param collectionPosition Position of item in collection that contains item;
* @return True if item is processable by this delegate.
*/
public boolean isForViewType(@NonNull final Object item, final int adapterPosition, final int collectionPosition) {
return true;
}
@Override
public long getItemId(@NonNull final List<Object> items, final int adapterPosition, final int collectionPosition) {
//noinspection unchecked
return getItemId((TItem) items.get(collectionPosition), adapterPosition, collectionPosition);
}
/**
* Returns unique ID of item to support stable ID's logic of RecyclerView's adapter.
*
* @param item Item in adapter;
* @param adapterPosition Position of item in adapter;
* @param collectionPosition Position of item in collection that contains item;
* @return Unique item ID.
*/
public long getItemId(@NonNull final TItem item, final int adapterPosition, final int collectionPosition) {
return 0;
}
@Override
public void onBindViewHolder(
@NonNull final RecyclerView.ViewHolder holder,
@NonNull final List<Object> items,
final int adapterPosition,
final int collectionPosition,
@NonNull final List<Object> payloads
) {
//noinspection unchecked
onBindViewHolder((TViewHolder) holder, (TItem) items.get(collectionPosition), adapterPosition, collectionPosition, payloads);
}
/**
* Binds item with payloads to created by this object ViewHolder.
*
* @param holder ViewHolder to bind item to;
* @param item Item in adapter;
* @param adapterPosition Position of item in adapter;
* @param collectionPosition Position of item in collection that contains item;
* @param payloads Payloads;
*/
public abstract void onBindViewHolder(
@NonNull final TViewHolder holder,
@NonNull final TItem item,
final int adapterPosition,
final int collectionPosition,
@NonNull final List<Object> payloads
);
}

View File

@ -0,0 +1,24 @@
package ru.touchin.roboswag.components.adapters
import android.support.v7.util.ListUpdateCallback
import android.support.v7.widget.RecyclerView
class OffsetAdapterUpdateCallback(private val adapter: RecyclerView.Adapter<*>, private val offsetProvider: () -> Int) : ListUpdateCallback {
override fun onInserted(position: Int, count: Int) {
adapter.notifyItemRangeInserted(position + offsetProvider(), count)
}
override fun onRemoved(position: Int, count: Int) {
adapter.notifyItemRangeRemoved(position + offsetProvider(), count)
}
override fun onMoved(fromPosition: Int, toPosition: Int) {
adapter.notifyItemMoved(fromPosition + offsetProvider(), toPosition + offsetProvider())
}
override fun onChanged(position: Int, count: Int, payload: Any?) {
adapter.notifyItemRangeChanged(position + offsetProvider(), count, payload)
}
}

View File

@ -0,0 +1,68 @@
package ru.touchin.roboswag.components.adapters;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import java.util.List;
/**
* Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders.
* Such delegates are creating and binding ViewHolders by position in adapter.
* Default {@link #getItemViewType} is generating on construction of object.
*
* @param <TViewHolder> Type of {@link RecyclerView.ViewHolder} of delegate.
*/
public abstract class PositionAdapterDelegate<TViewHolder extends RecyclerView.ViewHolder> extends AdapterDelegate<TViewHolder> {
@Override
public boolean isForViewType(@NonNull final List<Object> items, final int adapterPosition, final int collectionPosition) {
return isForViewType(adapterPosition);
}
/**
* Returns if object is processable by this delegate.
*
* @param adapterPosition Position of item in adapter;
* @return True if item is processable by this delegate.
*/
public abstract boolean isForViewType(final int adapterPosition);
@Override
public long getItemId(@NonNull final List<Object> objects, final int adapterPosition, final int itemsOffset) {
return getItemId(adapterPosition);
}
/**
* Returns unique ID of item to support stable ID's logic of RecyclerView's adapter.
*
* @param adapterPosition Position of item in adapter;
* @return Unique item ID.
*/
public long getItemId(final int adapterPosition) {
return 0;
}
@Override
public void onBindViewHolder(
@NonNull final RecyclerView.ViewHolder holder,
@NonNull final List<Object> items,
final int adapterPosition,
final int collectionPosition,
@NonNull final List<Object> payloads
) {
//noinspection unchecked
onBindViewHolder((TViewHolder) holder, adapterPosition, payloads);
}
/**
* Binds position with payloads to ViewHolder.
*
* @param holder ViewHolder to bind position to;
* @param adapterPosition Position of item in adapter;
* @param payloads Payloads.
*/
public void onBindViewHolder(@NonNull final TViewHolder holder, final int adapterPosition, @NonNull final List<Object> payloads) {
//do nothing by default
}
}

1
sample/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

32
sample/build.gradle Normal file
View File

@ -0,0 +1,32 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion versions.compileSdk
defaultConfig {
applicationId "ru.touchin.roboswag.components"
minSdkVersion versions.minSdk
targetSdkVersion versions.compileSdk
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.android.support:appcompat-v7:$versions.supportLibrary"
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
}
ext.buildScriptsDir = "$rootDir/BuildScripts"
apply from: "$buildScriptsDir/gradle/staticAnalysis.gradle"

21
sample/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="ru.touchin.roboswag.components">
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,12 @@
package ru.touchin.roboswag.components
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}

View File

@ -0,0 +1,35 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0"/>
<item
android:color="#00000000"
android:offset="1.0"/>
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1"/>
</vector>

View File

@ -0,0 +1,171 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z"/>
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
</vector>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Some files were not shown because too many files have changed in this diff Show More