Search code examples
cachingxamarin.formsakavache

Akavache always calling the fetchFunc


I have the following code using Akavache in a Xamarin app and it's not behaving the way I would think it should. Probably my misunderstanding of how it should be but it's driving me crazy.

So in my viewmodel I'm making the call to FetchNewsCategories and specifying a cache of 5 minutes for the item. What I'd expect to happen is that if the cache item is not there, it would make a call to the fetchFunc (ie. FetchNewsCategoriesAsync) but if I call the service any number of times inside the cache timeout of 5 minutes, it should just give me the cached item and not do the server call. In all cases that I've tried, it keeps doing the rest call and never gives me the cached item. I've also tried this with GetAndFetchLatest and if there is a cached item, it doesn't make the rest call but it also doesn't make the call in the subscribe event in the viewmodel. Any ideas what I'm doing wrong here?

EDIT: I tested this same code on Android (Nexus 5 KitKat API19) and it's working flawlessly. I'm going to reset my IOS emulator and see if something was just out of whack.

NewsService.cs

public static async Task<ServiceResponse<List<ArticleCategoryInfo>>> FetchNewsCategoriesAsync(BlogSourceType blogSource)
    {
        return await ServiceClient.POST<List<ArticleCategoryInfo>>(Config.ApiUrl + "news/categories", new
        {
            ModuleId = Int32.Parse(Config.Values[blogSource == BlogSourceType.News ? ConfigKeys.KEY_NEWS_MODULE_ID : ConfigKeys.KEY_BLOG_MODULE_ID])
        });
    }

public static IObservable<ServiceResponse<List<ArticleCategoryInfo>>> FetchNewsCategories(BlogSourceType blogSource)
    {
        var cache = BlobCache.LocalMachine;
        var cachedCategories = cache.GetOrFetchObject("categories" + blogSource,
                    async () => await FetchNewsCategoriesAsync(blogSource),
                    DateTime.Now.AddMinutes(5));

        return cachedCategories;
    }

NewsViewModel.cs

public async Task LoadCategories()
{
var cachedCategories = NewsService.FetchNewsCategories(blogSource);

cachedCategories.Subscribe((obj) => { Device.BeginInvokeOnMainThread(() => DisplayCategories(obj.Result,"Subscribe"));});
return;
}

private void DisplayCategories(IList<ArticleCategoryInfo> categories, string source)
    {
        Categories.Clear();
        System.Diagnostics.Debug.WriteLine("Redisplaying categories from " + source);
        foreach (var item in categories)
        {
            Categories.Add(item);
        }
    }

Solution

  • Just wanted to add my resolution to the issue I experienced above for reference to others with this problem.

    The ServiceResponse object that I was trying to cache had an HttpResponseMessage in it which I suspect was causing a serialization error, probably a cyclical reference, so it never did get cached and ended up calling the endpoint every time. I ended up putting an [IgnoreDataMemberAttribute] on that property so it wasn't serialized and the problems went away.

    I ended up handling the subscribe in the following manner to handle errors and to make sure the activity indicator bound to the IsBusy property was updated properly.

    public async Task LoadActivities(bool refresh)
        {
    
            IsBusy = true;
    
            if (refresh) OlderThanJournalId = int.MaxValue;
    
            var cached = ActivityService.FetchJournalItems(GroupId, OlderThanJournalId, refresh);
    
            cached.Subscribe((result) => { Device.BeginInvokeOnMainThread(() => { 
                DisplayActivities(result); 
            }); }, (err) => HandleError(err), () => IsBusy = false);
    
        }
    
    public void HandleError(Exception ex) {
            IsBusy = false;
            DialogService.ShowErrorToast(AppResources.ErrorMessage, "Unable to load activity stream.");
            System.Diagnostics.Debug.WriteLine(ex.Message);
        }
    
    private void DisplayActivities(ServiceResponse<List<JournalItem>> response)
        {
            if (!response.IsConnected) {
                DialogService.ShowInfoToast(AppResources.ErrorMessage, AppResources.NotConnected);
                return;
            }
            if (!response.Authorized) {
                App.LoginManager.Logout();
            }
            Activities.Clear();
            foreach (var item in response.Result)
            {
                Activities.Add(item);
            }
        }
    

    BeginInvokeOnMainThread is used to make sure that the updates to the ObservableCollection in DisplayActivities are seen in the UI.