diff --git a/settings.gradle b/settings.gradle index 177ef735..f28458d6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -49,3 +49,5 @@ include ':logger' include ':logger:aspect' include ':logger:annotations' include ':synchronizer' +include ':synchronizer:model' +include ':synchronizer:core' diff --git a/synchronizer/api/current.api b/synchronizer/api/current.api deleted file mode 100644 index d826f659..00000000 --- a/synchronizer/api/current.api +++ /dev/null @@ -1,29 +0,0 @@ -// Signature format: 4.0 -package com.urlaunched.synchronizer { - - public abstract class BaseSynchronizer { - ctor public BaseSynchronizer(); - method public final suspend Object? emitCancelDelete(com.urlaunched.synchronizer.Synchronizable value, kotlin.coroutines.Continuation); - method public final suspend Object? emitDelete(com.urlaunched.synchronizer.Synchronizable value, kotlin.coroutines.Continuation); - method public final suspend Object? emitUpdate(com.urlaunched.synchronizer.Synchronizable value, kotlin.coroutines.Continuation); - method public final kotlinx.coroutines.flow.MutableSharedFlow> getCancelDeleteModel(); - method public final kotlinx.coroutines.flow.MutableSharedFlow> getDeletedModel(); - method public final kotlinx.coroutines.flow.MutableSharedFlow> getUpdateModel(); - method public inline , ID> java.util.List synchronize(java.util.List, kotlinx.coroutines.CoroutineScope viewModelScope); - method public inline , reified R extends com.urlaunched.synchronizer.Synchronizable, ID> java.util.List synchronize(java.util.List, kotlinx.coroutines.CoroutineScope viewModelScope, kotlin.reflect.KClass relatedType, optional kotlin.jvm.functions.Function2 map); - method public inline , ID> kotlinx.coroutines.flow.Flow> synchronize(kotlinx.coroutines.flow.Flow>, kotlinx.coroutines.CoroutineScope viewModelScope); - method public inline , reified R extends com.urlaunched.synchronizer.Synchronizable, ID> kotlinx.coroutines.flow.Flow> synchronize(kotlinx.coroutines.flow.Flow>, kotlinx.coroutines.CoroutineScope viewModelScope, kotlin.reflect.KClass relatedType, optional kotlin.jvm.functions.Function2 map); - method public suspend inline , ID> void synchronize(T, kotlin.jvm.functions.Function1 onUpdate); - method public suspend inline , reified R extends com.urlaunched.synchronizer.Synchronizable, ID> void synchronize(T, kotlin.jvm.functions.Function1 onUpdate, kotlin.reflect.KClass relatedType, kotlin.jvm.functions.Function1 map); - property public final kotlinx.coroutines.flow.MutableSharedFlow> cancelDeleteModel; - property public final kotlinx.coroutines.flow.MutableSharedFlow> deletedModel; - property public final kotlinx.coroutines.flow.MutableSharedFlow> updateModel; - } - - public interface Synchronizable { - method public T getId(); - property public abstract T id; - } - -} - diff --git a/synchronizer/.gitignore b/synchronizer/core/.gitignore similarity index 100% rename from synchronizer/.gitignore rename to synchronizer/core/.gitignore diff --git a/synchronizer/core/api/current.api b/synchronizer/core/api/current.api new file mode 100644 index 00000000..47ee9972 --- /dev/null +++ b/synchronizer/core/api/current.api @@ -0,0 +1,27 @@ +// Signature format: 4.0 +package com.urlaunched.synchronizer.core { + + public final class ModelSynchronizer { + method public suspend Object? emitCancelDelete(com.urlaunched.android.synchonizer.model.Synchronizable value, kotlin.coroutines.Continuation); + method public suspend Object? emitDelete(com.urlaunched.android.synchonizer.model.Synchronizable value, kotlin.coroutines.Continuation); + method public suspend Object? emitUpdate(com.urlaunched.android.synchonizer.model.Synchronizable value, kotlin.coroutines.Continuation); + method public kotlinx.coroutines.flow.MutableSharedFlow> getCancelDeleteModel(); + method public kotlinx.coroutines.flow.MutableSharedFlow> getDeletedModel(); + method public kotlinx.coroutines.flow.MutableSharedFlow> getUpdateModel(); + method public inline > getUpdatesFlow(); + method public suspend inline , ID> void synchronize(ID id, kotlin.jvm.functions.Function1 onUpdate); + method public suspend inline , ID> void synchronize(kotlin.jvm.functions.Function0> getList, kotlin.jvm.functions.Function1,? extends kotlin.Unit> onUpdate); + method public suspend inline , reified RELATED extends com.urlaunched.android.synchonizer.model.Synchronizable, ID> void synchronizeRelated(kotlin.jvm.functions.Function0> getList, kotlin.jvm.functions.Function2 map, kotlin.jvm.functions.Function1,? extends kotlin.Unit> onUpdate); + property public final kotlinx.coroutines.flow.MutableSharedFlow> cancelDeleteModel; + property public final kotlinx.coroutines.flow.MutableSharedFlow> deletedModel; + property public final kotlinx.coroutines.flow.MutableSharedFlow> updateModel; + field public static final com.urlaunched.synchronizer.core.ModelSynchronizer INSTANCE; + } + + public final class ModelSynchronizerKt { + method public static inline , ID> kotlinx.coroutines.flow.Flow> synchronize(kotlinx.coroutines.flow.Flow>, kotlinx.coroutines.CoroutineScope coroutineScope, optional kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher); + method public static inline , reified RELATED extends com.urlaunched.android.synchonizer.model.Synchronizable, ID> kotlinx.coroutines.flow.Flow> synchronizeRelated(kotlinx.coroutines.flow.Flow>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2 mapRelatedToActual, kotlin.jvm.functions.Function1 mapActualToRelated, optional kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher); + } + +} + diff --git a/synchronizer/build.gradle b/synchronizer/core/build.gradle similarity index 94% rename from synchronizer/build.gradle rename to synchronizer/core/build.gradle index 16a56fd1..265ec739 100644 --- a/synchronizer/build.gradle +++ b/synchronizer/core/build.gradle @@ -9,7 +9,7 @@ plugins { } android { - namespace 'com.urlaunched.synchronizer' + namespace 'com.urlaunched.synchronizer.core' compileSdk gradleDependencies.compileSdk defaultConfig { @@ -49,6 +49,9 @@ metalava { } dependencies { + // Models + api project(":synchronizer:model") + // Android implementation libs.androidCoreDependencies.core implementation libs.androidCoreDependencies.appcompat @@ -57,7 +60,6 @@ dependencies { // Kotlin implementation(platform(libs.kotlinDependencies.bom)) implementation libs.kotlinDependencies.coroutines - implementation libs.kotlinDependencies.serialization // Paging implementation libs.pagingDependencies.core diff --git a/synchronizer/consumer-rules.pro b/synchronizer/core/consumer-rules.pro similarity index 100% rename from synchronizer/consumer-rules.pro rename to synchronizer/core/consumer-rules.pro diff --git a/synchronizer/core/gradle.properties b/synchronizer/core/gradle.properties new file mode 100644 index 00000000..69539852 --- /dev/null +++ b/synchronizer/core/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=synchronizer-core +POM_NAME=Data syncronizer core +POM_PACKAGING=aar +VERSION_NAME=LOCAL \ No newline at end of file diff --git a/synchronizer/proguard-rules.pro b/synchronizer/core/proguard-rules.pro similarity index 100% rename from synchronizer/proguard-rules.pro rename to synchronizer/core/proguard-rules.pro diff --git a/synchronizer/src/main/AndroidManifest.xml b/synchronizer/core/src/main/AndroidManifest.xml similarity index 100% rename from synchronizer/src/main/AndroidManifest.xml rename to synchronizer/core/src/main/AndroidManifest.xml diff --git a/synchronizer/core/src/main/java/com/urlaunched/synchronizer/core/ModelSynchronizer.kt b/synchronizer/core/src/main/java/com/urlaunched/synchronizer/core/ModelSynchronizer.kt new file mode 100644 index 00000000..7589332b --- /dev/null +++ b/synchronizer/core/src/main/java/com/urlaunched/synchronizer/core/ModelSynchronizer.kt @@ -0,0 +1,259 @@ +package com.urlaunched.synchronizer.core + +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.filter +import androidx.paging.map +import com.urlaunched.android.synchonizer.model.Synchronizable +import com.urlaunched.synchronizer.core.ModelSynchronizer.cancelDeleteModel +import com.urlaunched.synchronizer.core.ModelSynchronizer.deletedModel +import com.urlaunched.synchronizer.core.ModelSynchronizer.updateModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +object ModelSynchronizer { + val updateModel: MutableSharedFlow> = MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val deletedModel: MutableSharedFlow> = MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val cancelDeleteModel: MutableSharedFlow> = MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + suspend inline fun , ID> synchronize( + crossinline getList: () -> List, + crossinline onUpdate: (updatedList: List) -> Unit + ) { + coroutineScope { + launch { + updateModel + .filterIsInstance() + .collectLatest { updatedData -> + val currentList = getList() + + onUpdate( + currentList.map { currentItem -> + if (currentItem.id == updatedData.id) { + updatedData + } else { + currentItem + } + } + ) + } + } + + launch { + val deletedItemsPositions = mutableMapOf() + + launch { + deletedModel + .filterIsInstance() + .collectLatest { deletedModel -> + val currentList = getList() + + currentList.indexOf(deletedModel).takeIf { it != -1 }?.let { index -> + deletedItemsPositions.put(deletedModel.id, index) + + onUpdate( + currentList.filter { item -> item.id != deletedModel.id } + ) + } + } + } + + launch { + cancelDeleteModel + .filterIsInstance() + .collectLatest { cancelDeleteModel -> + val currentList = getList() + + deletedItemsPositions[cancelDeleteModel.id]?.let { index -> + deletedItemsPositions.remove(cancelDeleteModel.id) + onUpdate( + currentList.toMutableList().apply { add(index, cancelDeleteModel) } + ) + } + } + } + } + } + } + + suspend inline fun , reified RELATED : Synchronizable, ID> synchronizeRelated( + crossinline getList: () -> List, + crossinline map: (ACTUAL, RELATED) -> ACTUAL, + crossinline onUpdate: (updatedList: List) -> Unit + ) { + updateModel + .filterIsInstance() + .collectLatest { updatedData -> + val currentList = getList() + + onUpdate( + currentList.map { currentItem -> + if (currentItem.id == updatedData.id) { + map(currentItem, updatedData) + } else { + currentItem + } + } + ) + } + } + + suspend inline fun , ID> synchronize( + id: ID, + crossinline onUpdate: (ACTUAL) -> Unit + ) { + updateModel + .filterIsInstance() + .filter { it.id == id } + .collectLatest { updatedItem -> + onUpdate(updatedItem) + } + } + + inline fun > getUpdatesFlow() = updateModel.filterIsInstance() + + suspend fun emitUpdate(value: Synchronizable<*>) { + updateModel.emit(value) + } + + suspend fun emitDelete(value: Synchronizable<*>) { + deletedModel.emit(value) + } + + suspend fun emitCancelDelete(value: Synchronizable<*>) { + cancelDeleteModel.emit(value) + } +} + +inline fun , reified RELATED : Synchronizable, ID> Flow>.synchronizeRelated( + coroutineScope: CoroutineScope, + crossinline mapRelatedToActual: (ACTUAL, RELATED) -> ACTUAL, + crossinline mapActualToRelated: (ACTUAL) -> RELATED, + coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO +): Flow> { + val accumulatedUpdates = MutableStateFlow>(mapOf()) + val updatedData = mutableSetOf() + + coroutineScope.launch(coroutineDispatcher) { + updateModel + .filterIsInstance() + .collectLatest { modelUpdate -> + accumulatedUpdates.update { currentData -> + currentData + Pair(modelUpdate.id, modelUpdate) + } + } + } + + return combine( + this + .onEach { + updatedData.clear() + } + .map { pagingData -> + pagingData.map { item -> + if (!updatedData.contains(item.id)) { + mapActualToRelated(item)?.let { updateModel.emit(it) } + updatedData.add(item.id) + } + + item + } + } + .cachedIn(coroutineScope), + accumulatedUpdates + ) { pagingData, updates -> + pagingData.map { item -> + updates[item.id]?.let { mapRelatedToActual(item, it) } ?: item + } + }.cachedIn(coroutineScope) +} + +inline fun , ID> Flow>.synchronize( + coroutineScope: CoroutineScope, + coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO +): Flow> { + val accumulatedUpdates = MutableStateFlow>(mapOf()) + val accumulatedDeletions = MutableStateFlow>(mapOf()) + val updatedData = mutableSetOf() + + coroutineScope.launch(coroutineDispatcher) { + updateModel + .filterIsInstance() + .collectLatest { modelUpdate -> + accumulatedUpdates.update { currentData -> + currentData + Pair(modelUpdate.id, modelUpdate) + } + } + } + + coroutineScope.launch(coroutineDispatcher) { + deletedModel + .filterIsInstance() + .collectLatest { modelDeletion -> + accumulatedDeletions.update { currentData -> + currentData + Pair(modelDeletion.id, modelDeletion) + } + } + } + + coroutineScope.launch(coroutineDispatcher) { + cancelDeleteModel + .filterIsInstance() + .collectLatest { modelDeletionCancellation -> + accumulatedDeletions.update { currentData -> + currentData - modelDeletionCancellation.id + } + } + } + + return combine( + this + .onEach { + updatedData.clear() + } + .map { pagingData -> + pagingData.map { item -> + if (!updatedData.contains(item.id)) { + updateModel.emit(item) + updatedData.add(item.id) + } + + item + } + } + .cachedIn(coroutineScope), + accumulatedUpdates, + accumulatedDeletions + ) { pagingData, updatedData, deletedData -> + pagingData + .filter { item -> + !deletedData.contains(item.id) + } + .map { item -> + updatedData[item.id] ?: item + } + }.cachedIn(coroutineScope) +} \ No newline at end of file diff --git a/synchronizer/gradle.properties b/synchronizer/gradle.properties deleted file mode 100644 index 298a5bcc..00000000 --- a/synchronizer/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -POM_ARTIFACT_ID=synchronizer -POM_NAME=Data syncronizer -POM_PACKAGING=aar -VERSION_NAME=LOCAL \ No newline at end of file diff --git a/synchronizer/model/.gitignore b/synchronizer/model/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/synchronizer/model/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/synchronizer/model/build.gradle b/synchronizer/model/build.gradle new file mode 100644 index 00000000..3f3fc472 --- /dev/null +++ b/synchronizer/model/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java-library' + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.ktlint) + alias(libs.plugins.metalava) + alias(libs.plugins.mavenPublish) +} +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +metalava { + sourcePaths.setFrom("src/main") + filename.set("api/current.api") +} \ No newline at end of file diff --git a/synchronizer/model/gradle.properties b/synchronizer/model/gradle.properties new file mode 100644 index 00000000..d47881de --- /dev/null +++ b/synchronizer/model/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=synchronizer-model +POM_NAME=Synchronizer models +POM_PACKAGING=aar +VERSION_NAME=LOCAL \ No newline at end of file diff --git a/synchronizer/model/src/main/java/com/urlaunched/android/synchonizer/model/Synchronizable.kt b/synchronizer/model/src/main/java/com/urlaunched/android/synchonizer/model/Synchronizable.kt new file mode 100644 index 00000000..60d9790f --- /dev/null +++ b/synchronizer/model/src/main/java/com/urlaunched/android/synchonizer/model/Synchronizable.kt @@ -0,0 +1,5 @@ +package com.urlaunched.android.synchonizer.model + +interface Synchronizable { + val id: T +} \ No newline at end of file diff --git a/synchronizer/src/main/java/com/urlaunched/synchronizer/BaseSynchronizer.kt b/synchronizer/src/main/java/com/urlaunched/synchronizer/BaseSynchronizer.kt deleted file mode 100644 index 02b439cb..00000000 --- a/synchronizer/src/main/java/com/urlaunched/synchronizer/BaseSynchronizer.kt +++ /dev/null @@ -1,242 +0,0 @@ -package com.urlaunched.synchronizer - -import androidx.paging.PagingData -import androidx.paging.cachedIn -import androidx.paging.filter -import androidx.paging.map -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlin.reflect.KClass - -abstract class BaseSynchronizer { - val updateModel: MutableSharedFlow> = MutableSharedFlow() - val deletedModel: MutableSharedFlow> = MutableSharedFlow() - val cancelDeleteModel: MutableSharedFlow> = MutableSharedFlow() - - inline fun , reified R : Synchronizable, ID> Flow>.synchronize( - viewModelScope: CoroutineScope, - relatedType: KClass, - crossinline map: (T, R) -> T = { t, _ -> t } - ): Flow> { - val localMap = MutableStateFlow>(mapOf()) - - viewModelScope.launch(Dispatchers.IO) { - updateModel - .filter { it is T || it is R } - .collectLatest { item -> - item.let { updatedItem -> - updatedItem as Synchronizable - - localMap.update { currentData -> - val updatedMap = currentData.toMutableMap() - val currentItem = updatedMap[updatedItem.id] - - val mappedItem = if (updatedItem is R) { - currentItem?.let { current -> - map(current, updatedItem) - } - } else { - updatedItem as T - } - - mappedItem?.let { updatedMap[updatedItem.id] = it } - updatedMap - } - } - } - } - - return combine( - this.cachedIn(viewModelScope), - localMap - ) { pagingData, localMapValue -> - pagingData.map { item -> - val updatedMap = localMap.value.toMutableMap() - updatedMap[item.id] = item - localMap.value = updatedMap - localMapValue[item.id] ?: item - } - }.cachedIn(viewModelScope) - } - - inline fun , ID> Flow>.synchronize( - viewModelScope: CoroutineScope - ): Flow> { - val updatedLocalMap = MutableStateFlow>(mapOf()) - val deletedLocalMap = MutableStateFlow>(mapOf()) - - viewModelScope.launch(Dispatchers.IO) { - updateModel.filter { it is T }.collectLatest { item -> - item.let { updatedItem -> - updatedItem as Synchronizable - - updatedLocalMap.update { currentData -> - val updatedMap = currentData.toMutableMap() - - updatedMap[updatedItem.id] = updatedItem as T - updatedMap - } - } - } - } - - viewModelScope.launch(Dispatchers.IO) { - deletedModel.filter { it is T }.collectLatest { item -> - item.let { deletedItem -> - deletedItem as Synchronizable - - deletedLocalMap.update { currentData -> - val deletedMap = currentData.toMutableMap() - - deletedMap[deletedItem.id] = deletedItem as T - deletedMap - } - } - } - } - - viewModelScope.launch(Dispatchers.IO) { - cancelDeleteModel.filter { it is T }.collectLatest { item -> - item.let { cancelDeleteItem -> - cancelDeleteItem as Synchronizable - - deletedLocalMap.update { currentData -> - val deletedMap = currentData.toMutableMap() - deletedMap.remove(cancelDeleteItem.id) - deletedMap - } - } - } - } - - return combine( - this.cachedIn(viewModelScope), - updatedLocalMap - ) { pagingData, updatedData -> - pagingData - .map { item -> - val updatedMap = updatedLocalMap.value.toMutableMap() - updatedMap[item.id] = item - updatedLocalMap.value = updatedMap - updatedData[item.id] ?: item - } - }.combine(deletedLocalMap) { pagingData, deletedData -> - pagingData.filter { item -> - deletedData.containsKey(item.id).not() - } - }.cachedIn(viewModelScope) - } - - inline fun , reified R : Synchronizable, ID> List.synchronize( - viewModelScope: CoroutineScope, - relatedType: KClass, - crossinline map: (T, R) -> T = { t, _ -> t } - ): List { - val localMap = MutableStateFlow>(mapOf()) - - viewModelScope.launch(Dispatchers.IO) { - updateModel - .filter { it is T || it is R } - .collectLatest { item -> - item.let { updatedItem -> - updatedItem as Synchronizable - - localMap.update { currentData -> - val updatedMap = currentData.toMutableMap() - val currentItem = updatedMap[updatedItem.id] - - val mappedItem = if (updatedItem is R) { - currentItem?.let { current -> - map(current, updatedItem) - } - } else { - updatedItem as T - } - - mappedItem?.let { updatedMap[updatedItem.id] = it } - updatedMap - } - } - } - } - - return localMap.value.values.toList() - } - - inline fun , ID> List.synchronize(viewModelScope: CoroutineScope): List { - val localMap = MutableStateFlow>(mapOf()) - - viewModelScope.launch(Dispatchers.IO) { - updateModel - .filter { it is T } - .collectLatest { item -> - item.let { updatedItem -> - updatedItem as Synchronizable - - localMap.update { currentData -> - val updatedMap = currentData.toMutableMap() - - val mappedItem = updatedItem as T - mappedItem.let { updatedMap[updatedItem.id] = it } - updatedMap - } - } - } - } - - return localMap.value.values.toList() - } - - suspend inline fun , reified R : Synchronizable, ID> T.synchronize( - crossinline onUpdate: (T) -> Unit, - relatedType: KClass, - crossinline map: (R) -> T - ) { - updateModel - .filterNotNull() - .filter { it is T || it is R } - .filter { it.id == this.id } - .collectLatest { updatedItem -> - when (updatedItem) { - is R -> { - onUpdate(map(updatedItem)) - } - - is T -> { - onUpdate(updatedItem) - } - } - } - } - - suspend inline fun , ID> T.synchronize(crossinline onUpdate: (T) -> Unit) { - updateModel - .filterNotNull() - .filter { it is T } - .filter { it.id == this.id } - .collectLatest { updatedItem -> - onUpdate(updatedItem as T) - } - } - - suspend fun emitUpdate(value: Synchronizable<*>) { - updateModel.emit(value) - } - - suspend fun emitDelete(value: Synchronizable<*>) { - deletedModel.emit(value) - } - - suspend fun emitCancelDelete(value: Synchronizable<*>) { - cancelDeleteModel.emit(value) - } -} \ No newline at end of file diff --git a/synchronizer/src/main/java/com/urlaunched/synchronizer/Synchronizable.kt b/synchronizer/src/main/java/com/urlaunched/synchronizer/Synchronizable.kt deleted file mode 100644 index c62d71a5..00000000 --- a/synchronizer/src/main/java/com/urlaunched/synchronizer/Synchronizable.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.urlaunched.synchronizer - -interface Synchronizable { - val id: T -} \ No newline at end of file