Kotlin Library for Async Data Loading and Caching

Related tags

Kotlin Store
Overview

Store 4

Maven Central

codecov

Store is a Kotlin library for loading data from remote and local sources.

The Problems:

  • Modern software needs data representations to be fluid and always available.
  • Users expect their UI experience to never be compromised (blocked) by new data loads. Whether an application is social, news or business-to-business, users expect a seamless experience both online and offline.
  • International users expect minimal data downloads as many megabytes of downloaded data can quickly result in astronomical phone bills.

A Store is a class that simplifies fetching, sharing, storage, and retrieval of data in your application. A Store is similar to the Repository pattern while exposing an API built with Coroutines that adheres to a unidirectional data flow.

Store provides a level of abstraction between UI elements and data operations.

Overview

A Store is responsible for managing a particular data request. When you create an implementation of a Store, you provide it with a Fetcher, a function that defines how data will be fetched over network. You can also define how your Store will cache data in-memory and on-disk. Since Store returns your data as a Flow, threading is a breeze! Once a Store is built, it handles the logic around data flow, allowing your views to use the best data source and ensuring that the newest data is always available for later offline use.

Store leverages multiple request throttling to prevent excessive calls to the network and disk cache. By utilizing Store, you eliminate the possibility of flooding your network with the same request while adding two layers of caching (memory and disk) as well as ability to add disk as a source of truth where you can modify the disk directly without going through Store (works best with databases that can provide observables sources like Jetpack Room, SQLDelight or Realm)

How to include in your project

Artifacts are hosted on Maven Central.

Latest version:
def store_version = "4.0.1"
//if using kotlin 1.5 (https://github.com/dropbox/Store/issues/263)
def store_version = "4.0.4-KT15"
Add the dependency to your build.gradle:
implementation "com.dropbox.mobile.store:store4:${store_version}"
Set the source & target compatibilities to 1.8
android {
    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
    ...
}

Fully Configured Store

Let's start by looking at what a fully configured Store looks like. We will then walk through simpler examples showing each piece:

StoreBuilder
    .from(
        fetcher = Fetcher.of { api.fetchSubreddit(it, "10").data.children.map(::toPosts) },
        sourceOfTruth = SourceOfTruth.of(
            reader = db.postDao()::loadPosts,
            writer = db.postDao()::insertPosts,
            delete = db.postDao()::clearFeed,
            deleteAll = db.postDao()::clearAllFeeds
        )
    ).build()

With the above setup you have:

  • In-memory caching for rotation
  • Disk caching for when users are offline
  • Throttling of API calls when parallel requests are made for the same resource
  • Rich API to ask for data whether you want cached, new or a stream of future data updates.

And now for the details:

Creating a Store

You create a Store using a builder. The only requirement is to include a Fetcher which is just a typealias to a function that returns a Flow<FetcherResult<ReturnType>>.

val store = StoreBuilder
        .from(Fetcher.ofFlow { articleId -> api.getArticle(articleId) }) // api returns Flow<Article>
        .build()

Store uses generic keys as identifiers for data. A key can be any value object that properly implements toString(), equals() and hashCode(). When your Fetcher function is called, it will be passed a particular Key value. Similarly, the key will be used as a primary identifier within caches (Make sure to have a proper hashCode()!!).

Note: We highly recommend using built-in types that implement equals and hashcode or Kotlin data classes for complex keys.

Public Interface - Stream

The primary function provided by a Store instance is the stream function which has the following signature:

fun stream(request: StoreRequest<Key>): Flow<StoreResponse<Output>>

Each stream call receives a StoreRequest object, which defines which key to fetch and which data sources to utilize. The response is a Flow of StoreResponse. StoreResponse is a Kotlin sealed class that can be either a Loading, Data or Error instance. Each StoreResponse includes an origin field which specifies where the event is coming from.

  • The Loading class only has an origin field. This can provide you information like "network is fetching data", which can be a good signal to activate the loading spinner in your UI.
  • The Data class has a value field which includes an instance of the type returned by Store.
  • The Error class includes an error field that contains the exception thrown by the given origin.

When an error happens, Store does not throw an exception, instead, it wraps it in a StoreResponse.Error type which allows Flow to continue so that it can still receive updates that might be triggered by either changes in your data source or subsequent fetch operations.

viewModelScope.launch {
  store.stream(StoreRequest.cached(key = key, refresh=true)).collect { response ->
    when(response) {
        is StoreResponse.Loading -> showLoadingSpinner()
        is StoreResponse.Data -> {
            if (response.origin == ResponseOrigin.Fetcher) hideLoadingSpinner()
            updateUI(response.value)
        }
        is StoreResponse.Error -> {
            if (response.origin == ResponseOrigin.Fetcher) hideLoadingSpinner()
            showError(response.error)
        }
    }
  }
}

For convenience, there are Store.get(key) and Store.fresh(key) extension functions.

  • suspend fun Store.get(key: Key): Value: This method returns a single value for the given key. If available, it will be returned from the in memory cache or the sourceOfTruth. An error will be thrown if no value is available in either the cache or sourceOfTruth, and the fetcher fails to load the data from the network.
  • suspend fun Store.fresh(key: Key): Value: This method returns a single value for the given key that is obtained by querying the fetcher. An error will be thrown if the fetcher fails to load the data from the network, regardless of whether any value is available in the cache or sourceOfTruth.
lifecycleScope.launchWhenStarted {
  val article = store.get(key)
  updateUI(article)
}

The first time you call to suspend store.get(key), the response will be stored in an in-memory cache and in the sourceOfTruth, if provided. All subsequent calls to store.get(key) with the same Key will retrieve the cached version of the data, minimizing unnecessary data calls. This prevents your app from fetching fresh data over the network (or from another external data source) in situations when doing so would unnecessarily waste bandwidth and battery. A great use case is any time your views are recreated after a rotation, they will be able to request the cached data from your Store. Having this data available can help you avoid the need to retain this in the view layer.

By default, 100 items will be cached in memory for 24 hours. You may pass in your own memory policy to override the default policy.

Skipping Memory/Disk

Alternatively, you can call store.fresh(key) to get a suspended result that skips the memory (and optional disk cache).

A good use case is overnight background updates use fresh() to make sure that calls to store.get() will not have to hit the network during normal usage. Another good use case for fresh() is when a user wants to pull to refresh.

Calls to both fresh() and get() emit one value or throw an error.

Stream

For real-time updates, you may also call store.stream() which returns a Flow<T> that emits each time a new item is returned from your store. You can think of stream as a way to create reactive streams that update when you db or memory cache updates

example calls:

lifecycleScope.launchWhenStarted {
    store.stream(StoreRequest.cached(3, refresh = false)) //will get cached value followed by any fresh values, refresh will also trigger network call if set to `true` even if the data is available in cache or disk.
        .collect {}
    store.stream(StoreRequest.fresh(3)) //skip cache, go directly to fetcher
        .collect {}
}

Inflight Debouncer

To prevent duplicate requests for the same data, Store offers an inflight debouncer. If the same request is made as a previous identical request that has not completed, the same response will be returned. This is useful for situations when your app needs to make many async calls for the same data at startup or when users are obsessively pulling to refresh. As an example, The New York Times news app asynchronously calls ConfigStore.get() from 12 different places on startup. The first call blocks while all others wait for the data to arrive. We have seen a dramatic decrease in the app's data usage after implementing this inflight logic.

Disk as Cache

Stores can enable disk caching by passing a SourceOfTruth into the builder. Whenever a new network request is made, the Store will first write to the disk cache and then read from the disk cache.

Disk as Single Source of Truth

Providing sourceOfTruth whose reader function can return a Flow<Value?> allows you to make Store treat your disk as source of truth. Any changes made on disk, even if it is not made by Store, will update the active Store streams.

This feature, combined with persistence libraries that provide observable queries (Jetpack Room, SQLDelight or Realm) allows you to create offline first applications that can be used without an active network connection while still providing a great user experience.

StoreBuilder
    .from(
        fetcher = Fetcher.of { api.fetchSubreddit(it, "10").data.children.map(::toPosts) },
        sourceOfTruth = SourceOfTruth.of(
            reader = db.postDao()::loadPosts,
            writer = db.postDao()::insertPosts,
            delete = db.postDao()::clearFeed,
            deleteAll = db.postDao()::clearAllFeeds
        )
    ).build()

Stores don’t care how you’re storing or retrieving your data from disk. As a result, you can use Stores with object storage or any database (Realm, SQLite, CouchDB, Firebase etc). Technically, there is nothing stopping you from implementing an in-memory cache for the "sourceOfTruth" implementation and instead have two levels of in-memory caching--one with inflated and one with deflated models, allowing for sharing of the “sourceOfTruth” cache data between stores.

If using SQLite we recommend working with Room which returns a Flow from a query

The above builder is how we recommend working with data on Android. With the above setup you have:

  • Memory caching with TTL & Size policies
  • Disk caching with simple integration with Room
  • In-flight request management
  • Ability to get cached data or bust through your caches (get() vs. fresh())
  • Ability to listen for any new emissions from network (stream)
  • Structured Concurrency through APIs build on Coroutines and Kotlin Flow

Configuring in-memory Cache

You can configure in-memory cache with the MemoryPolicy:

StoreBuilder
    .from(
        fetcher = Fetcher.of { api.fetchSubreddit(it, "10").data.children.map(::toPosts) },
        sourceOfTruth = SourceOfTruth.of(
            reader = db.postDao()::loadPosts,
            writer = db.postDao()::insertPosts,
            delete = db.postDao()::clearFeed,
            deleteAll = db.postDao()::clearAllFeeds
        )
    ).cachePolicy(
        MemoryPolicy.builder()
            .setMemorySize(10)
            .setExpireAfterAccess(10.minutes) // or setExpireAfterWrite(10.minutes)
            .build()
    ).build()
  • setMemorySize(maxSize: Long) sets the maximum number of entries to be kept in the cache before starting to evict the least recently used items.
  • setExpireAfterAccess(expireAfterAccess: Duration) sets the maximum time an entry can live in the cache since the last access, where "access" means reading the cache, adding a new cache entry, and replacing an existing entry with a new one. This duration is also known as time-to-idle (TTI).
  • setExpireAfterWrite(expireAfterWrite: Duration) sets the maximum time an entry can live in the cache since the last write, where "write" means adding a new cache entry and replacing an existing entry with a new one. This duration is also known as time-to-live (TTL).

Note that setExpireAfterAccess and setExpireAfterWrite cannot both be set at the same time.

Clearing store entries

You can delete a specific entry by key from a store, or clear all entries in a store.

Store with no sourceOfTruth

val store = StoreBuilder
  .from(
    fetcher = Fetcher.of { key: String ->
      api.fetchData(key)
  }).build()

The following will clear the entry associated with the key from the in-memory cache:

store.clear("10")

The following will clear all entries from the in-memory cache:

store.clearAll()

Store with sourceOfTruth

When store has a sourceOfTruth, you'll need to provide the delete and deleteAll functions for clear(key) and clearAll() to work:

StoreBuilder
    .from(
        fetcher = Fetcher.of { api.fetchData(key) },
        sourceOfTruth = SourceOfTruth.of(
            reader = dao::loadData,
            writer = dao::writeData,
            delete = dao::clearDataByKey,
            deleteAll = dao::clearAllData
        )
    ).build()

The following will clear the entry associated with the key from both the in-memory cache and the sourceOfTruth:

store.clear("myKey")

The following will clear all entries from both the in-memory cache and the sourceOfTruth:

store.clearAll()
Comments
  • [BUG] Crash in Store 4.0 with Kotlin 1.5.0-RC

    [BUG] Crash in Store 4.0 with Kotlin 1.5.0-RC

    Describe the bug Application crash after update Kotlin from 1.4.32 to 1.5.0-RC, no other changes in code.

    Catch Exception:

    E/AndroidRuntime: FATAL EXCEPTION: main
        Process: ru.superapplication.android.app.debug, PID: 18674
        java.lang.NoSuchMethodError: No static method getHours(I)D in class Lkotlin/time/DurationKt; or its super classes (declaration of 'kotlin.time.DurationKt' appears in /data/app/~~l_eMNOPYSnv8XUlDP7ycgA==/ru.superapplication.android.app.debug-ziiU-phO368T6tA7R1CseQ==/base.apk!classes33.dex)
            at com.dropbox.android.external.store4.StoreDefaults.<clinit>(StoreDefaults.kt:15)
            at com.dropbox.android.external.store4.RealStoreBuilder.<init>(StoreBuilder.kt:91)
            at com.dropbox.android.external.store4.StoreBuilder$Companion.from(StoreBuilder.kt:76)
    
    bug 
    opened by SteinerOk 30
  • [Feature Request] Improved Support for Pull to Refresh

    [Feature Request] Improved Support for Pull to Refresh

    Is your feature request related to a problem? Please describe. The documentation states "Another good use case for fresh() is when a user wants to pull to refresh." While this is correct, there is a drawback to using fresh() for pull to refresh events: We don't get the Loading or Error states from the StoreResponse. This may mean implementing additional logic around showing the user the loading state or error state in the UI when using both stream(), for the primary flow of data, and fresh() for the pull to refresh event.

    Describe the solution you'd like I created the following extensions to be used in a pull refresh scenario and have found they work well. Since pull to refresh is common, perhaps these would be good to integrate into Store somehow?

    /**
     * Use to trigger a network refresh for your Store. An example use-case is for pull to refresh functionality.
     * This is best used in conjunction with `Flow.collectRefreshFlow` to prevent this returned Flow from living
     * on longer than necessary and conflicting with your primary Store Flow.
     */
    fun <Key : Any, Output : Any> Store<Key, Output>.refreshFlow(key: Key) = stream(StoreRequest.fresh(key))
    
    /**
     * Helper to observe the loading state of a Store refresh call triggered via `Store.refreshFlow`. This Flow will
     * automatically be cancelled after the refresh is successful or has an error.
     *
     * @param checkLoadingState Lambda providing the current StoreResponse for the refresh (Loading, Error, or Data) allowing
     * you to decide when to show/hide your loading indicator.
     */
    suspend fun <T> Flow<StoreResponse<T>>.collectRefreshFlow(checkLoadingState: suspend (StoreResponse<T>) -> Unit) {
        onEach { storeResponse ->
            checkLoadingState(storeResponse)
        }.first { storeResponse ->
            storeResponse.isData() || storeResponse.isError()
        }
    }
    

    Examples of the solution in use Below is an example of how refreshFlow is used alongside the main flow:

    fun getUserDataFlow(): Flow<StoreResponse<List<UserData>>> {
       return myStore.stream(StoreRequest.cached(Key("123"), refresh = true))
    }
    
    fun refreshUserDataFlow(): Flow<StoreResponse<List<UserData>>> {
       return myStore.refreshFlow(Key("123"))
    }
    

    And finally an example of how collectRefreshFlow is used in a ViewModel alongside the main flow:

    class MyViewModel {
       val userDataLiveData = userRepository.getUserDataFlow()
          .onEach { storeResponse ->
                setLoadingSpinnerVisibility(storeResponse)
                setEmptyStateVisibility(storeResponse)
           }
           .filterIsInstance<StoreResponse.Data<List<UserData>>>()
           .mapLatest { storeResponse ->
                storeResponse.dataOrNull() ?: emptyList()
           }
           .flowOn(Dispatchers.IO)
           .asLiveData().distinctUntilChanged()
       }
    
       fun onRetryLoadUserData() = viewModelScope.launch {
          if (networkIsConnected()) {
             userRepository.refreshUserDataFlow()
                .collectRefreshFlow { storeResponse -> setLoadingSpinnerVisibility(storeResponse) }
          }
       }
    }
    

    Additional context I feel these two extensions provide more power/flexibility in the pull to refresh scenario than simply calling the suspending fresh() method. Does it seem like these or something similar could fit into the Store API?

    enhancement 
    opened by alexandercasal 25
  • Cache Rewrite

    Cache Rewrite

    Resolves #14

    This is a rewrite of the cache module in Kotlin. The new implementation supports sized-based and time-based (time-to-live and time-to-idle) evictions.

    Cache and Builder APIs

    interface Cache<in Key, Value> {
    
        /**
         * Returns the value associated with [key] in this cache, or null if there is no
         * cached value for [key].
         */
        fun get(key: Key): Value?
    
        /**
         * Returns the value associated with [key] in this cache if exists,
         * otherwise gets the value by invoking [loader], associates the value with [key] in the cache,
         * and returns the cached value.
         *
         * Note that if an unexpired value for the [key] is present by the time the [loader] returns
         * the new value, the existing value won't be replaced by the new value. Instead the existing
         * value will be returned.
         */
        fun get(key: Key, loader: () -> Value): Value
    
        /**
         * Associates [value] with [key] in this cache. If the cache previously contained a
         * value associated with [key], the old value is replaced by [value].
         */
        fun put(key: Key, value: Value)
    
        /**
         * Discards any cached value for key [key].
         */
        fun invalidate(key: Key)
    
        /**
         * Discards all entries in the cache.
         */
        fun invalidateAll()
    
        /**
         * Main entry point for creating a [Cache].
         */
        interface Builder {
    
            /**
             * Specifies that each entry should be automatically removed from the cache once a fixed duration
             * has elapsed after the entry's creation or the most recent replacement of its value.
             *
             * When [duration] is zero, the cache's max size will be set to 0
             * meaning no values will be cached.
             */
            fun expireAfterWrite(duration: Long, unit: TimeUnit): Builder
    
            /**
             * Specifies that each entry should be automatically removed from the cache once a fixed duration
             * has elapsed after the entry's creation, the most recent replacement of its value, or its last
             * access.
             *
             * When [duration] is zero, the cache's max size will be set to 0
             * meaning no values will be cached.
             */
            fun expireAfterAccess(duration: Long, unit: TimeUnit): Builder
    
            /**
             * Specifies the maximum number of entries the cache may contain.
             * Cache eviction policy is based on LRU - i.e. least recently accessed entries get evicted first.
             *
             * When [size] is 0, entries will be discarded immediately and no values will be cached.
             *
             * If not set, cache size will be unlimited.
             */
            fun maximumCacheSize(size: Long): Builder
    
            /**
             * Guides the allowed concurrent update operations. This is used as a hint for internal sizing,
             * actual concurrency will vary.
             *
             * If not set, default concurrency level is 16.
             */
            fun concurrencyLevel(concurrencyLevel: Int): Builder
    
            /**
             * Specifies [Clock] for this cache.
             *
             * This is useful for controlling time in tests
             * where a fake [Clock] implementation can be provided.
             *
             * A [SystemClock] will be used if not specified.
             */
            fun clock(clock: Clock): Builder
    
            /**
             * Builds a new instance of [Cache] with the specified configurations.
             */
            fun <K : Any, V : Any> build(): Cache<K, V>
        }
    }
    

    Note that currently only APIs required by RealStore are supported. Please let know if more APIs are required.

    Usage

    // build a new cache
    val cache = Cache.Builder.newBuilder()
                .expireAfterWrite(30, TimeUnit.MINUTES)
                .expireAfterAccess(24, TimeUnit.HOURS)
                .maximumCacheSize(100)
                .build<Long, String>()
    
    // cache a value
    cache.put(1, "dog")
    
    // get cached value by key
    cache.get(1)
    
    // get cached value by key, using the provided loader to compute and cache the value if none exists
    val value = cache.get(2) { "cat" }
    
    // remove a cached value by key
    cache.invalidate(1)
    
    // remove all values from cache
    cache.invalidateAll()
    

    Tests

    ./gradlew cache:test

    100% Test Coverage:

    cache4-coverage

    Migration

    All usages have been switched to use the new cache implementation.

    I've also converted the remaining Java files to Kotlin, except MultiParserTest.java which is commented out.


    Will update cache/README.md in a followup.

    opened by ychescale9 20
  • Handle fetchers that don't throw an exception

    Handle fetchers that don't throw an exception

    Exceptions aren't good. I think the Kotlin community is coalescing around returning a sealed class with error information instead of throwing an exception to indicate that a function has an error. That's what Store does with the StoreResponse class. However, a StoreResponse.Error object will only be returned if the fetcher throws an exception. Now the libraries commonly used by a fetcher, such as Retrofit or Ktor, throw exceptions on error (bad Ktor, no biscuit). But what if someone wants to use a library that returns a sealed class to indicate success or failure?

    For example, if MyLibrary returns a MyLibraryResult.Success or a MyLibraryResult.Error, how should the fetcher or Store handle it?

    • The app could define a custom exception which the fetcher throws when it receives an error, but we're trying to get away from exceptions. Introducing an exception between two libraries that don't use exceptions doesn't sound right.
    • The fetcher could return the MyLibraryResult object (whether or not there was an error), but then Store doesn't know there was a failure. Also the persister has to deal with unwrapping the data while storing it, and handling any errors.
    • There could be some direct way for the fetcher to tell Store there was a failure. The fetcher lambda could be an extension function on an interface, perhaps with a reportError() method. If this is called then the fetcher's return value is ignored and it is treated as an error.
    • The fetcher returns a StoreResponse.Error object, which Store detects and treats the same as if an exception was thrown.
    • The StoreBuilder functions, in addition to the key and output types, has a third type for errors. If the fetcher returns an object of this class, it treats it as an error. For example: StoreBuilder.fromNotFlow<String, Data, MyLibraryResult.Error>(). If the fetcher succeeds, it unwraps the data from the result class and returns it. If it fails, it returns an error object.

    The StoreResponse class would need to be modified to handle this new kind of error. Maybe there are two error subclasses, one for exceptions and one for non-exceptions. Maybe StoreResponse.Error can contain either a Throwable or some other error class.

    discussion 
    opened by ebrowne72 19
  • What's wrong with stream or how to use it properly?

    What's wrong with stream or how to use it properly?

    Let's review this simple example

      fun fetchFromCollection(keys: Set<MySerializedType>){
            someNonblockingScope.launch {
                for(key in keys) {
                    myStore.steam(StoreRequest.cached(key, true)).collect {
                        if (it.origin == ResponseOrigin.Fetcher && it !is StoreResponse.Loading)
                            Log.d("CHECKING TRIGGER", "$key triggered")
                    }
                }
            }
        }
    
    

    the expected outcome should be that CHECKING TRIGGER will trigger once for each unique key even if fetchFromCollection executed several times (each time always unique keys)?

    If I execute fetchFromCollection with one unique set of keys it will run as expected, but if I execute second time fetchFromCollection with another different set of keys it will trigger CHECKING TRIGGER condition more than once(new set of unique keys).

    What I miss? Why it doesn't work as expected?

    And then if execute fetchFromCollection three, four times and so on each time with new set of keys stream will just hang with Loading state forever.

    I pushed this example project demonstrating the issue in full scale.

    needs-info 
    opened by atonamy 18
  • `StoreRequest.cached(1, refresh = false)` invoke `fetcher` when cache available?

    `StoreRequest.cached(1, refresh = false)` invoke `fetcher` when cache available?

    It seems that even if we set refresh flag to false in StoreRequest.cached, we still get the latest data from the fetcher. Is this the correct behavior?

    Test code:

    runBlocking {
        // Store with cache, without persister
        val store = StoreBuilder.fromNonFlow { key: Int -> key }.build()
    
        // Cache data
        store.fresh(1)
    
        // Stream data
        store.stream(StoreRequest.cached(1, refresh = false)).collect {
            println(it.toString())
        }
    }
    

    Output:

    Data(value=1, origin=Cache)
    Loading(origin=Fetcher)
    Data(value=1, origin=Fetcher)
    
    bug 
    opened by lcdsmao 17
  • Compatibility issue with Kotlin 1.6

    Compatibility issue with Kotlin 1.6

    Describe the bug Duration API has been made stable in Kotlin 1.6.0, and some methods have been deleted. As result, the following error appears:

    java.lang.NoSuchMethodError: No static method toDuration(DLjava/util/concurrent/TimeUnit;)J in class Lkotlin/time/DurationKt; or its super classes (declaration of 'kotlin.time.DurationKt' appears in /data/app/com.enki.Enki750g-dL2OkPW0zzwa1CQrnIAsnQ==/base.apk!classes22.dex)
            at com.dropbox.android.external.store4.MemoryPolicy.<clinit>(MemoryPolicy.kt:116)
            at com.dropbox.android.external.store4.StoreDefaults.<clinit>(StoreDefaults.kt:23)
            at com.dropbox.android.external.store4.RealStoreBuilder.<init>(StoreBuilder.kt:91)
            at com.dropbox.android.external.store4.StoreBuilder$Companion.from(StoreBuilder.kt:76)
            at com.webedia.food.config.ConfigManager.<init>(ConfigManager.kt:51)
    

    To Reproduce

    Create a store with the default options.

    Expected behavior Should not crash

    Infos

    • Store Version: 4.0.2-KT15
    bug 
    opened by bishiboosh 16
  • [BUG] Non-global scope leaks caller

    [BUG] Non-global scope leaks caller

    Describe the bug Disclaimer: I'm no expert in coroutine scoping, so there's a great chance this is user error.
    When I build a Store and declare a Scope that is not GlobalScope, I found that calling fetch on Store will leak my caller.

    To Reproduce Here's a minimal activity that reproduces the issue. On config change (screen rotation), MainActivity is retained--- forever as far as I can tell. If I omit the scope argument on the Store builder or set the scope to global, I get the behavior I expect, which is that the fetch is cancelled and no scope is retained.

    I tried following the ref chain down- it seems the scope assigned to FetcherController (which is the scope I passed into the builder) is ultimately retaining my Activity.

    class MainActivity : AppCompatActivity() {
        var time: Long = 0
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            time = System.currentTimeMillis()
    
            lifecycleScope.launch {
                store.fresh(time)
            }
        }
    }
    
    object Repository {
        val store = StoreBuilder.from(
            fetcher = nonFlowFetcher<Long, Long> {
                delay(5000)
                FetcherResult.Data(it)        }
        ).scope(CoroutineScope(Dispatchers.Main)).build()
    }
    

    Expected behavior My expectation is that when I cancel the scope around my Store call (store.fresh()), I'd see a cancellation of any child jobs or Flows that Store has spawned for me, and I would not be retained by Store's scope.

    ** Additional context** Here's a heap dump! heap_dump_several_main_activities.txt

    bug 
    opened by elizrus 14
  • Setting in-memory cache expiry is useless when source of truth is provided

    Setting in-memory cache expiry is useless when source of truth is provided

    After reading #73 and the RealStore implementation, I realized it's effectively useless to set expireAfterAccess or expireAfterWrite on the MemoryPolicy when a sourceOfTruth is provided to the store, as the disk value will be immediately backfilled to the cache after cache expiration. From the user's perspective, they'll get the data from the disk instead of cache (slightly slower) when cache has expired but the fetcher won't be triggered (unless they bypass Store and manually delete the entry from disk).

    This behavior is inconsistent to when sourceOfTruth == null, in which case cache expiry will trigger the fetcher (thanks to piggybackOnly).

    Should we try to make these 2 branches (with or without sourceOfTruth) behave consistently in terms of the effect of cache expiry?

    I remember the old Store had a networkBeforeStale() API for triggering the fetcher if the disk entry is stale. Does it make sense to support this (on both memory cache and sourceOfTruth?)?

    I understand that cache expiry and disk entry becoming stale could be interpreted as different concepts so unifying the behavior with or without sourceOfTruth might cause other confusions...

    opened by ychescale9 14
  • Support non exception errors from fetcher

    Support non exception errors from fetcher

    Non-working PR to show the proposed way of adding support for allowing the fetcher to communicate errors not via exceptions.

    The main complication here is in the builder API. we already have 2 builders because we support a builder with and without source of truth (to change to generic type). If we went this way we would either need to support 4 builder or we will need to change the builder API to force providing the source of truth (and the new translator) in the builder's constructor

    opened by eyalgu 13
  • [BUG] Downstream flows never complete for Fetcher Flows that don't emit anything

    [BUG] Downstream flows never complete for Fetcher Flows that don't emit anything

    Describe the bug When a fetcher flow just completes without emitting any data the downstream flows will never complete. SourceOfTruth reader won't be called

    To Reproduce fetcher = { key -> flow {} }

    Expected behavior Store should detect when the fetcher flow completes and allow the rest of the process to continue as normal.

    Store Version 4.0.0-alpha06

    Additional context Fetchers flows that don't emit anything are extremely useful when manually dealing with caches that can't utilize the OkHttp cache.

    bug 
    opened by hansenji 12
  • Add aclassen as co-author of KMP translation

    Add aclassen as co-author of KMP translation

    Please see our contributing guidelines (contributing.md) primarily make sure to sign our cla as we cannot accept code externally without a signed cla

    https://opensource.dropbox.com/cla/

    opened by matt-ramotar 2
  • Enable converter from network to local

    Enable converter from network to local

        it looks like we can have a version of the converter that has:
    

    fromNetworkToSOT fromSOTToCommon fromCommonToSOT

    This would skip the need to make a converter going from network to common. Raises the question of whether that should be default as well. Why make someone pass a converter from network to common when they can give you a converter from network right to SOT

    Originally posted by @digitalbuddha in https://github.com/MobileNativeFoundation/Store/pull/499#discussion_r1056335601

    opened by matt-ramotar 0
  • java.lang.NoClassDefFoundError: Failed resolution of: Lkotlinx/atomicfu/AtomicFU

    java.lang.NoClassDefFoundError: Failed resolution of: Lkotlinx/atomicfu/AtomicFU

    When migrating from Store4 to Store5, the below-mentioned crash happened in the Android platform.

    java.lang.NoClassDefFoundError: Failed resolution of: Lkotlinx/atomicfu/AtomicFU;
            at org.mobilenativefoundation.store.store5.impl.SourceOfTruthWithBarrier.<init>(SourceOfTruthWithBarrier.kt:57)
            at org.mobilenativefoundation.store.store5.impl.RealStore.<init>(RealStore.kt:56)
            at org.mobilenativefoundation.store.store5.impl.RealStoreBuilder.build(RealStoreBuilder.kt:38)
    Caused by: java.lang.ClassNotFoundException: Didn't find class "kotlinx.atomicfu.AtomicFU" on path
    

    In the latest Store5, since the Kotlin version 1.7.21 is used, I have updated the Compose Compiler Version to 1.4.0-alpha02. But in the AtomicFU, they have mentioned turning on IR transformation by setting the below-mentioned properties in the gradle.properties file for Kotlin version >= 1.7.20.

    kotlinx.atomicfu.enableJvmIrTransformation=true // for JVM IR transformation
    kotlinx.atomicfu.enableJsIrTransformation=true // for JS IR transformation
    

    But in the Store5 gradle.properties file, it was configured as mentioned below for the AtomicFU version 0.18.5.

    kotlinx.atomicfu.enableJvmIrTransformation=false
    kotlinx.atomicfu.enableJsIrTransformation=false
    kotlin.js.compiler=ir
    

    I'm not sure whether this might be the reason for the above-mentioned crash in the Android platform.

    bug 
    opened by SridharShanmugam 8
  • Enable override of eager conflict resolution strategy

    Enable override of eager conflict resolution strategy

    Problem

    Android docs have guidance on conflict resolution once network is available. But they don’t consider cases of certain network services remaining unavailable. See https://developer.android.com/topic/architecture/data-layer/offline-first

    Example

    Imagine a user who can read from server but whose writes to server always fail. Normally we will eagerly resolve conflicts by pushing local writes before requesting latest values. But in this case the user can not push the local changes. We could:

    1. Discard latest server value and continue to persist local changes until server write succeeds
    2. Overwrite local changes with latest server value

    Discussion (see #store-core)

    @matt-ramotar - I think 1 makes the most sense. My preference is to defer to server. On first glance for me deferring to server meant overwriting with latest server value. However I think that is incorrect. I think deferring to server really means not discarding local changes until server receives them and makes a decision.

    @digitalbuddha - Some folks might want destructive reads even when local writes exist.

    Decision

    Defer to server by default but enable configuration when:

    • [x] Online + No Changes
    • [x] Online + Changes
    • [x] Offline + No Changes
    • [x] Offline + Changes
    • [ ] Can't Update + No Changes
    • [ ] Can't Update + Changes
    • [ ] Can't Read + No Changes
    • [ ] Can't Read + Changes
    enhancement 
    opened by matt-ramotar 0
  • Move noop logic to converter

    Move noop logic to converter

        maybe move the noop logic to the converter
    

    Originally posted by @digitalbuddha in https://github.com/MobileNativeFoundation/Store/pull/496#discussion_r1053357158

    opened by matt-ramotar 0
  • Collapse r/w requests into Store.Request

    Collapse r/w requests into Store.Request

        I think eventually we should collapse to
    

    Store.Request.Write and Store.Request.Read

    same with the response

    Originally posted by @digitalbuddha in https://github.com/MobileNativeFoundation/Store/pull/496#discussion_r1053355301

    opened by matt-ramotar 0
Releases(5.0.0-alpha03)
Owner
Dropbox
Dropbox
async/await for Android built upon coroutines introduced in Kotlin 1.1

Async/Await A Kotlin library for Android to write asynchronous code in a simpler and more reliable way using async/await approach, like: async { pr

MetaLab 411 Dec 22, 2022
AsyncSport - AsyncSports Async sports is very simple application that shows athletes video feeds

AsyncLabs Interview Solution ?? Writing AsyncLabs Interview Solution App using A

David Innocent 0 Jan 7, 2022
Very simple Kotlin caching library

Very simple Kotlin caching library

Ji Sungbin 4 Jun 15, 2022
A composite Github Action to execute the Kotlin Script with compiler plugin and dependency caching!

Kotlin Script Github Action Kotlin can also be used as a scripting language, which is more safer, concise, and fun to write than bash or python. Githu

Suresh 9 Nov 28, 2022
Easy to use cryptographic framework for data protection: secure messaging with forward secrecy and secure data storage. Has unified APIs across 14 platforms.

Themis provides strong, usable cryptography for busy people General purpose cryptographic library for storage and messaging for iOS (Swift, Obj-C), An

Cossack Labs 1.6k Jan 8, 2023
Image loading for Android backed by Kotlin Coroutines.

An image loading library for Android backed by Kotlin Coroutines. Coil is: Fast: Coil performs a number of optimizations including memory and disk cac

Coil 8.8k Jan 7, 2023
A basic application demonstrating IPFS for collaborative data analysis, from the perspective of a Data Analysis Provider.

Spacebox A basic application demonstrating IPFS for collaborative data analysis, from the perspective of a Data Analysis Provider. Description This pr

null 0 Jan 15, 2022
The most complete and powerful data-binding library and persistence infra for Kotlin 1.3, Android & Splitties Views DSL, JavaFX & TornadoFX, JSON, JDBC & SQLite, SharedPreferences.

Lychee (ex. reactive-properties) Lychee is a library to rule all the data. ToC Approach to declaring data Properties Other data-binding libraries Prop

Mike 112 Dec 9, 2022
Utility for developers and QAs what helps minimize time wasting on writing the same data for testing over and over again. Made by Stfalcon

Stfalcon Fixturer A Utility for developers and QAs which helps minimize time wasting on writing the same data for testing over and over again. You can

Stfalcon LLC 31 Nov 29, 2021
Clean MVVM with eliminating the usage of context from view models by introducing hilt for DI and sealed classes for displaying Errors in views using shared flows (one time event), and Stateflow for data

Clean ViewModel with Sealed Classes Following are the purposes of this repo Showing how you can remove the need of context in ViewModels. I. By using

Kashif Mehmood 22 Oct 26, 2022
Kotlin Multiplatform project that gets network data from Food2Fork.ca

Food2Fork Recipe App This is the codebase for a Kotlin Multiplatform Mobile course. [Watch the course](https://codingwithmitch.com/courses/kotlin-mult

Mitch Tabian 317 Dec 30, 2022
ViewModel LiveData Sample - Sample of using ViewModel, LiveData and Data Binding

ViewModel_LiveData_Sample Sample Code for Lesson 8 using ViewModel, LiveData and

null 0 Mar 18, 2022
Filmesflix - Project made during the NTT DATA Android Developer bootcamp. Developing knowledge in MVVM and Clear Architecture

FilmesFlix Projeto criado para o módulo de MVVM e Clean Architecture no Bootcamp

Italo Bruno 0 Feb 12, 2022
Show weather data for the current location [Apollo Agriculture Interview Solution], for the Senior Android Engineer Role

Apollo Agriculture Android Take Home Assignment Writing Apollo Agriculture App using Android Architecture Components, in 100% Kotlin, using Android Je

Juma Allan 23 Nov 23, 2022
A music player UI am designing in Jetpack Compose. The data is being fetched from Deezer's API.

Jetpack-Compose-MusicPlayer-UI ?? - Still under development This is a small project I am doing to interact with and explore Jetpack Compose components

Kagiri Charles 11 Nov 29, 2022
Course_modularizing_android_apps - Multi-module demo app that gets data from a Dota2 api

Work in progress Multi-module demo app that gets data from a Dota2 api. Module n

Julio Ribeiro 1 Dec 30, 2021
Location-history-viewer - Small compose-desktop app to view data from google's location history

Google Location History Takeout Viewer This application provides a minimalistic

Chris Stelzmüller 3 Jun 23, 2022
AppConversorMoedas - The currency conversion using an API to bring the data up to date

LAB - Criando um app de conversor moedas/cambio com Kotlin. O curso pode ser ace

Davi Braga 0 Jan 1, 2022