Added modules: api-logansquare, lifecycle-common, lifecycle-rx

This commit is contained in:
Denis Karmyshakov 2018-08-21 12:17:05 +03:00
parent 3a990daa86
commit 4ea6ed857b
49 changed files with 1585 additions and 390 deletions

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

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,27 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion versions.compileSdk
defaultConfig {
minSdkVersion versions.minSdk
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
repositories {
maven { url "http://dl.bintray.com/touchin/touchin-tools" }
}
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,11 +1,11 @@
buildscript {
ext.kotlin_version = '1.2.60'
ext.kotlin_version = '1.2.61'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.3'
classpath 'com.android.tools.build:gradle:3.1.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@ -24,7 +24,7 @@ task clean(type: Delete) {
ext {
versions = [
compileSdk : 27,
minSdk : 19,
minSdk : 16,
supportLibrary: '27.1.1',
navigation : '1.0.0-alpha04',
lifecycle : '1.1.1',

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,2 @@
<manifest
package="ru.touchin.lifecycle"/>

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,3 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="ru.touchin.lifecycle"/>

View File

@ -1,4 +1,4 @@
package ru.touchin.roboswag.components.utils.destroyable
package ru.touchin.livedata.destroyable
import io.reactivex.Completable
import io.reactivex.Flowable

View File

@ -1,4 +1,4 @@
package ru.touchin.roboswag.components.utils.destroyable
package ru.touchin.livedata.destroyable
import io.reactivex.Completable
import io.reactivex.Flowable

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.livedata.event.CompletableEvent
import ru.touchin.livedata.event.MaybeEvent
import ru.touchin.livedata.event.ObservableEvent
import ru.touchin.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.livedata.event.CompletableEvent
import ru.touchin.livedata.event.MaybeEvent
import ru.touchin.livedata.event.ObservableEvent
import ru.touchin.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.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.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.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.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>

View File

@ -4,7 +4,7 @@ android {
compileSdkVersion versions.compileSdk
defaultConfig {
minSdkVersion 16
minSdkVersion versions.minSdk
}
compileOptions {

View File

@ -1,10 +1,11 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion versions.compileSdk
defaultConfig {
minSdkVersion 16
minSdkVersion versions.minSdk
}
compileOptions {

View File

@ -1,39 +0,0 @@
/*
* 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.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.
*/
public interface OnFragmentStartedListener {
/**
* Calls by child fragment (added via {@link android.support.v4.app.FragmentManager}) on it'sstart.
*
* @param fragment Child fragment which called this method.
*/
void onFragmentStarted(@NonNull Fragment fragment);
}

View File

@ -29,6 +29,7 @@ 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;
/**
@ -36,7 +37,7 @@ import ru.touchin.roboswag.components.utils.UiUtils;
* 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;

View File

@ -25,7 +25,6 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.ArraySet;
import android.support.v7.app.AppCompatActivity;
import android.view.MenuItem;
import java.util.Set;
@ -96,13 +95,9 @@ public abstract class BaseActivity extends AppCompatActivity {
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
} else {
return super.onOptionsItemSelected(item);
}
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
public void addOnBackPressedListener(@NonNull final OnBackPressedListener onBackPressedListener) {
@ -123,18 +118,4 @@ public abstract class BaseActivity extends AppCompatActivity {
super.onBackPressed();
}
/*
* 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,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

@ -20,6 +20,8 @@
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;
@ -27,8 +29,10 @@ 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;
@ -41,6 +45,7 @@ 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;
@ -54,24 +59,15 @@ import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
* @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 ViewFragment<TActivity> {
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 boolean inDebugMode;
private static long acceptableUiCalculationTime = 100;
/**
* Enables debugging features like serialization of {@link #getState()} every creation.
*/
public static void setInDebugMode() {
inDebugMode = true;
}
/**
* Sets acceptable UI calculation time so there will be warnings in logs if ViewController's inflate/layout actions will take more than that time.
* Works only if {@link #setInDebugMode()} called.
* It's 100ms by default.
*/
public static void setAcceptableUiCalculationTime(final long acceptableUiCalculationTime) {
@ -113,6 +109,8 @@ public class ViewControllerFragment<TActivity extends FragmentActivity, TState e
@Nullable
private ActivityResult pendingActivityResult;
private boolean appeared;
/**
* Returns specific {@link Parcelable} which contains state of fragment and it's {@link ViewController}.
*
@ -128,19 +126,19 @@ public class ViewControllerFragment<TActivity extends FragmentActivity, TState e
return viewControllerClass;
}
@SuppressWarnings("unchecked")
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(!isChildFragment());
//noinspection unchecked
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 (inDebugMode) {
if (BuildConfig.DEBUG) {
state = reserialize(state);
}
} else {
@ -148,43 +146,6 @@ public class ViewControllerFragment<TActivity extends FragmentActivity, TState e
}
}
@NonNull
private ViewController createViewController(
@NonNull final FragmentActivity activity,
@NonNull final PlaceholderView 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 = inDebugMode ? 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 (inDebugMode) {
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 final View onCreateView(
@ -199,7 +160,7 @@ public class ViewControllerFragment<TActivity extends FragmentActivity, TState e
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
//noinspection ConstantConditions
viewController = createViewController(requireActivity(), (PlaceholderView) getView(), savedInstanceState);
viewController = createViewController(requireActivity(), (ViewGroup) getView(), savedInstanceState);
viewController.onCreate();
if (pendingActivityResult != null) {
viewController.onActivityResult(pendingActivityResult.requestCode, pendingActivityResult.resultCode, pendingActivityResult.data);
@ -235,25 +196,33 @@ public class ViewControllerFragment<TActivity extends FragmentActivity, TState e
}
}
@SuppressLint("RestrictedApi")
@Override
protected void onStart(@NonNull final View view, @NonNull final TActivity activity) {
super.onStart(view, activity);
public void onStart() {
super.onStart();
if (!appeared && isMenuVisible()) {
onAppear();
}
if (viewController != null) {
viewController.onStart();
}
}
@Override
protected void onAppear(@NonNull final View view, @NonNull final TActivity activity) {
super.onAppear(view, activity);
/**
* 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
protected void onResume(@NonNull final View view, @NonNull final TActivity activity) {
super.onResume(view, activity);
public void onResume() {
super.onResume();
if (viewController != null) {
viewController.onResume();
}
@ -281,8 +250,8 @@ public class ViewControllerFragment<TActivity extends FragmentActivity, TState e
}
@Override
protected void onPause(@NonNull final View view, @NonNull final TActivity activity) {
super.onPause(view, activity);
public void onPause() {
super.onPause();
if (viewController != null) {
viewController.onPause();
}
@ -297,20 +266,27 @@ public class ViewControllerFragment<TActivity extends FragmentActivity, TState e
savedInstanceState.putParcelable(VIEW_CONTROLLER_STATE_EXTRA, state);
}
@Override
protected void onDisappear(@NonNull final View view, @NonNull final TActivity activity) {
super.onDisappear(view, activity);
/**
* 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
protected void onStop(@NonNull final View view, @NonNull final TActivity activity) {
public void onStop() {
if (appeared) {
onDisappear();
}
if (viewController != null) {
viewController.onStop();
}
super.onStop(view, activity);
super.onStop();
}
@Override
@ -339,6 +315,66 @@ public class ViewControllerFragment<TActivity extends FragmentActivity, TState e
}
}
@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);
}
}
}
private static class PlaceholderView extends FrameLayout {
@NonNull
@ -353,7 +389,7 @@ public class ViewControllerFragment<TActivity extends FragmentActivity, TState e
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (inDebugMode && lastMeasureTime == 0) {
if (BuildConfig.DEBUG && lastMeasureTime == 0) {
lastMeasureTime = SystemClock.uptimeMillis();
}
}
@ -361,7 +397,7 @@ public class ViewControllerFragment<TActivity extends FragmentActivity, TState e
@Override
protected void onDraw(@NonNull final Canvas canvas) {
super.onDraw(canvas);
if (inDebugMode && lastMeasureTime > 0) {
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);

View File

@ -1,229 +0,0 @@
/*
* 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.arch.lifecycle.Lifecycle;
import android.os.Bundle;
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.View;
import android.view.ViewGroup;
import ru.touchin.roboswag.components.navigation.OnFragmentStartedListener;
import ru.touchin.roboswag.core.log.Lc;
import ru.touchin.roboswag.core.utils.BiConsumer;
/**
* Created by Gavriil Sitnikov on 21/10/2015.
* Non-background fragment that have specific activity as a parent.
*
* @param <TActivity> Type of activity which to such fragment could be attached.
*/
public abstract class ViewFragment<TActivity extends FragmentActivity> extends Fragment implements OnFragmentStartedListener {
private boolean appeared;
/**
* Returns if fragment have parent fragment.
*
* @return Returns true if fragment is in some fragment's children stack.
*/
public boolean isChildFragment() {
return getParentFragment() != null;
}
/**
* Returns specific activity which to this fragment could be attached.
*
* @return Returns parent activity.
*/
@SuppressWarnings("unchecked")
@Nullable
protected final TActivity getBaseActivity() {
if (getActivity() == null) {
return null;
}
try {
return (TActivity) getActivity();
} catch (final ClassCastException exception) {
Lc.assertion(exception);
return null;
}
}
@NonNull
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
throw new IllegalStateException("Method onCreateView() should be overridden");
}
@Override
@CallSuper
public void onFragmentStarted(@NonNull final Fragment fragment) {
//do nothing
}
@Override
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getView() == null || getBaseActivity() == null) {
Lc.assertion("View and activity shouldn't be null");
}
}
private void callMethodAfterInstantiation(@NonNull final BiConsumer<View, TActivity> action) {
if (getView() == null || getBaseActivity() == null) {
Lc.assertion("View and activity shouldn't be null");
return;
}
try {
action.accept(getView(), getBaseActivity());
} catch (final Exception exception) {
Lc.assertion(exception);
}
}
@Deprecated
@Override
public void onStart() {
super.onStart();
callMethodAfterInstantiation(this::onStart);
}
/**
* Replacement of {@link #onStart} with non null activity as first parameter.
*
* @param view Instantiated view.
* @param activity Activity which fragment attached to.
*/
@CallSuper
@SuppressWarnings("RestrictedApi")
//RestrictedApi: we need isMenuVisible() to check analytics rightly!
protected void onStart(@NonNull final View view, @NonNull final TActivity activity) {
if (getParentFragment() instanceof OnFragmentStartedListener) {
((OnFragmentStartedListener) getParentFragment()).onFragmentStarted(this);
} else if (activity instanceof OnFragmentStartedListener) {
((OnFragmentStartedListener) activity).onFragmentStarted(this);
}
if (!appeared && isMenuVisible()) {
onAppear(view, activity);
}
}
/**
* 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.
*
* @param view Instantiated view.
* @param activity Activity which fragment attached to.
*/
protected void onAppear(@NonNull final View view, @NonNull final TActivity activity) {
appeared = true;
}
@Deprecated
@Override
public void onResume() {
super.onResume();
callMethodAfterInstantiation(this::onResume);
}
/**
* Replacement of {@link #onResume} with non null activity as first parameter.
*
* @param view Instantiated view.
* @param activity Activity which fragment attached to.
*/
@CallSuper
protected void onResume(@NonNull final View view, @NonNull final TActivity activity) {
//do nothing
}
@Override
public void setMenuVisibility(final boolean menuVisible) {
super.setMenuVisibility(menuVisible);
if (getBaseActivity() != null && getView() != null) {
final boolean started = getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED);
if (!appeared && menuVisible && started) {
onAppear(getView(), getBaseActivity());
}
if (appeared && (!menuVisible || !started)) {
onDisappear(getView(), getBaseActivity());
}
}
}
@Deprecated
@Override
public void onPause() {
callMethodAfterInstantiation(this::onPause);
super.onPause();
}
/**
* Replacement of {@link #onPause} with non null activity as first parameter.
*
* @param view Instantiated view.
* @param activity Activity which fragment attached to.
*/
@CallSuper
protected void onPause(@NonNull final View view, @NonNull final TActivity activity) {
// do nothing
}
/**
* 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.
*
* @param view Instantiated view.
* @param activity Activity which fragment attached to.
*/
protected void onDisappear(@NonNull final View view, @NonNull final TActivity activity) {
appeared = false;
}
@Deprecated
@Override
public void onStop() {
callMethodAfterInstantiation(this::onStop);
super.onStop();
}
/**
* Replacement of {@link #onStop} with non null activity as first parameter.
*
* @param view Instantiated view.
* @param activity Activity which fragment attached to.
*/
@CallSuper
protected void onStop(@NonNull final View view, @NonNull final TActivity activity) {
if (appeared) {
onDisappear(view, activity);
}
}
}

View File

@ -3,14 +3,16 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 27
compileSdkVersion versions.compileSdk
defaultConfig {
applicationId "ru.touchin.roboswag.components"
minSdkVersion 16
targetSdkVersion 27
minSdkVersion versions.minSdk
targetSdkVersion versions.compileSdk
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
@ -21,6 +23,7 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation "com.android.support:appcompat-v7:$versions.supportLibrary"
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
}

View File

@ -1 +1 @@
include ':sample', ':utils', ':logging', ':navigation', ':storable'
include ':sample', ':utils', ':logging', ':navigation', ':storable', ':api-logansquare', ':lifecycle-common', ':lifecycle-rx'

View File

@ -4,7 +4,7 @@ android {
compileSdkVersion versions.compileSdk
defaultConfig {
minSdkVersion 16
minSdkVersion versions.minSdk
}
compileOptions {

View File

@ -4,7 +4,7 @@ android {
compileSdkVersion versions.compileSdk
defaultConfig {
minSdkVersion 16
minSdkVersion versions.minSdk
}
compileOptions {

View File

@ -1,19 +0,0 @@
package ru.touchin.roboswag.core.utils;
import android.support.annotation.Nullable;
/**
* A functional interface (callback) that accepts two values (of possibly different types).
* @param <T1> the first value type
* @param <T2> the second value type
*/
public interface BiConsumer<T1, T2> {
/**
* Performs an operation on the given values.
* @param t1 the first value
* @param t2 the second value
* @throws Exception on error
*/
void accept(@Nullable T1 t1, @Nullable T2 t2) throws Exception;
}

View File

@ -0,0 +1,146 @@
/*
* 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.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Process;
import android.support.annotation.NonNull;
import android.telephony.TelephonyManager;
/**
* Utility class that is providing common methods related to android device.
*/
public final class DeviceUtils {
/**
* Detects active network type.
*
* @param context Application context
* @return Active network type {@link NetworkType}
*/
@NonNull
public static NetworkType getNetworkType(@NonNull final Context context) {
if (context.checkPermission(Manifest.permission.ACCESS_NETWORK_STATE, Process.myPid(), Process.myUid())
!= PackageManager.PERMISSION_GRANTED) {
return NetworkType.UNKNOWN;
}
final ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
@SuppressLint("MissingPermission") final NetworkInfo info = cm.getActiveNetworkInfo();
if (info == null || !info.isConnected()) {
return NetworkType.NONE;
}
if (info.getType() == ConnectivityManager.TYPE_WIFI) {
return NetworkType.WI_FI;
}
if (info.getType() == ConnectivityManager.TYPE_MOBILE) {
final int networkType = info.getSubtype();
switch (networkType) {
case TelephonyManager.NETWORK_TYPE_GPRS:
case TelephonyManager.NETWORK_TYPE_EDGE:
case TelephonyManager.NETWORK_TYPE_CDMA:
case TelephonyManager.NETWORK_TYPE_1xRTT:
case TelephonyManager.NETWORK_TYPE_IDEN:
return NetworkType.MOBILE_2G;
case TelephonyManager.NETWORK_TYPE_UMTS:
case TelephonyManager.NETWORK_TYPE_EVDO_0:
case TelephonyManager.NETWORK_TYPE_EVDO_A:
case TelephonyManager.NETWORK_TYPE_HSDPA:
case TelephonyManager.NETWORK_TYPE_HSUPA:
case TelephonyManager.NETWORK_TYPE_HSPA:
case TelephonyManager.NETWORK_TYPE_EVDO_B:
case TelephonyManager.NETWORK_TYPE_EHRPD:
case TelephonyManager.NETWORK_TYPE_HSPAP:
return NetworkType.MOBILE_3G;
case TelephonyManager.NETWORK_TYPE_LTE:
case 19: // NETWORK_TYPE_LTE_CA is hide
return NetworkType.MOBILE_LTE;
case TelephonyManager.NETWORK_TYPE_UNKNOWN:
default:
return NetworkType.UNKNOWN;
}
}
return NetworkType.UNKNOWN;
}
/**
* Detects if some network connected.
*
* @param context Application context
* @return true if network connected, false otherwise.
*/
public static boolean isNetworkConnected(@NonNull final Context context) {
return getNetworkType(context) != NetworkType.NONE;
}
private DeviceUtils() {
}
/**
* Available network types.
*/
public enum NetworkType {
/**
* Mobile 2G network.
*/
MOBILE_2G("2g"),
/**
* Mobile 3G network.
*/
MOBILE_3G("3g"),
/**
* Mobile LTE network.
*/
MOBILE_LTE("lte"),
/**
* Wi-Fi network.
*/
WI_FI("Wi-Fi"),
/**
* Unknown network type.
*/
UNKNOWN("unknown"),
/**
* No network.
*/
NONE("none");
@NonNull
private final String name;
NetworkType(@NonNull final String name) {
this.name = name;
}
/**
* @return Network type readable name.
*/
@NonNull
public String getName() {
return name;
}
}
}