Implementing ModernHttpClient in MvvmCross

Earlier this week Paul Betts started a new open source project called ModernHttpClient which is a Xamarin wrapper around two native libs.

https://github.com/paulcbetts/ModernHttpClient

“This library brings the latest platform-specific networking libraries to Xamarin applications via a custom HttpClient handler. Write your app using System.Net.Http, but drop this library in and it will go drastically faster. This is made possible by two native libraries:
On iOS, AFNetworking 1.3.3
On Android, via OkHttp 1.2.1″

While Paul does claim your app will go drastically faster he’s not actually very specific about why it will go drastically faster but after a query I’m now aware that for iOS the AFNetworking library has better thread handling than the standard httpclient. So after some tests I confirmed that for single requests the webclient is no faster than the standard client, I’m yet to test performance in an app with many simultaneous client requests but in theory it should be faster.

If you’re using the Image Downloader in MvvmCross implementing the library is a little tricky, but luckily Stuart has done a great job of keeping it extensible.

To get started you’ll need to clone the repo but because it has external projects you’ll need to use the recursive command.

UPDATE, there’s an easier way to get it: Paul Betts Comment

git clone –recursive https://github.com/paulcbetts/ModernHttpClient.git

Once you’ve got a copy of it you’ll need to build both the Xamarin binding for AFNetworking and the ModernHttpClient.

The binding solution is in vendor/AFNetworking and the ModernHttpClient is in the root directory.

Now that you’ve built both the assemblies you need to add them to your Xamarin.iOS solution.

After a little dive into the MvvmCross source code I discovered that there’s a simple interface that I can implement and register. If you’re not sure what I’m talking about you can read up on MvvmCross plugins here: https://github.com/MvvmCross/MvvmCross/wiki/MvvmCross-plugins

1
2
3
4
public interface IMvxHttpFileDownloader
{ 
    void RequestDownload(string url, string downloadPath, Action success, Action<Exception> error);
}

So in the Xamarin.iOS project create a plugins directory and add two files MvxFastHttpFileDownloader and MvxFastFileDownloadRequest.

Because the standard MvvmCross plugins have already registered the interface IMvxhttpFileDownloader you need to make sure you register your downloader last, so put it at the bottom of the InitializeLastChance overide in your setup.cs file.

1
2
3
4
5
6
7
protected override void InitializeLastChance ()
{
    Cirrious.MvvmCross.Plugins.DownloadCache.PluginLoader.Instance.EnsureLoaded();
    Cirrious.MvvmCross.Plugins.File.PluginLoader.Instance.EnsureLoaded();
    Mvx.RegisterSingleton<IMvxHttpFileDownloader>(() => new MvxFastHttpFileDownloader());
    base.InitializeLastChance();
}

 

MvxFastHttpFileDownloader

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
using System;
using Cirrious.CrossCore.Core;
using Cirrious.MvvmCross.Plugins.DownloadCache;
using System.Collections.Generic;
using System.Linq;
 
namespace Ridland.MvvmCross.Plugins
{
    public class MvxFastHttpFileDownloader 
        : MvxLockableObject
            , IMvxHttpFileDownloader
    {
        private readonly Dictionary<MvxFastFileDownloadRequest, bool> _currentRequests =
            new Dictionary<MvxFastFileDownloadRequest, bool>();
 
        private const int DefaultMaxConcurrentDownloads = 30;
        private readonly int _maxConcurrentDownloads;
        private readonly Queue<MvxFastFileDownloadRequest> _queuedRequests = new Queue<MvxFastFileDownloadRequest>();
 
        public MvxFastHttpFileDownloader(int maxConcurrentDownloads = DefaultMaxConcurrentDownloads)
        {
            _maxConcurrentDownloads = maxConcurrentDownloads;
        }
 
        public void RequestDownload(string url, string downloadPath, Action success, Action<Exception> error)
        {
            var request = new MvxFastFileDownloadRequest(url, downloadPath);
            request.DownloadComplete += (sender, args) =>
            {
                OnRequestFinished(request);
                success();
            };
            request.DownloadFailed += (sender, args) =>
            {
                OnRequestFinished(request);
                error(args.Value);
            };
 
            RunSyncOrAsyncWithLock( () =>
                                   {
                _queuedRequests.Enqueue(request);
                if (_currentRequests.Count < _maxConcurrentDownloads)
                {
                    StartNextQueuedItem();
                }
            });
        }
 
        private void OnRequestFinished(MvxFastFileDownloadRequest request)
        {
            RunSyncOrAsyncWithLock(() =>
                                   {
                _currentRequests.Remove(request);
                if (_queuedRequests.Any())
                {
                    StartNextQueuedItem();
                }
            });
        }
 
        private void StartNextQueuedItem()
        {
            if (_currentRequests.Count >= _maxConcurrentDownloads)
                return;
 
            RunSyncOrAsyncWithLock(() =>
                                   {
                if (_currentRequests.Count >= _maxConcurrentDownloads)
                    return;
 
                if (!_queuedRequests.Any())
                    return;
 
                var request = _queuedRequests.Dequeue();
                _currentRequests.Add(request, true);
                request.Start();
            });
        }
    }
}

MvxFastFileDownloadRequest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
using System;
using Cirrious.MvvmCross.Plugins.DownloadCache;
using Cirrious.CrossCore.Core;
using System.Net.Http;
using ModernHttpClient;
using System.Threading.Tasks;
using System.IO;
 
namespace Ridland.MvvmCross.iOS.Plugins
{
    public class MvxFastFileDownloadRequest
    {
        public MvxFastFileDownloadRequest(string url, string downloadPath)
        {
            Url = url;
            DownloadPath = downloadPath;
        }
 
        public string DownloadPath { get; private set; }
        public string Url { get; private set; }
 
        public event EventHandler<MvxFileDownloadedEventArgs> DownloadComplete;
        public event EventHandler<MvxValueEventArgs<Exception>> DownloadFailed;
 
        HttpClient CreateClient()
        {
            return new HttpClient(new AFNetworkHandler());
        }
 
        public void Start()
        {
            var client = CreateClient();
 
            Task<HttpResponseMessage> result;
 
            result = client.GetAsync(Url);
 
            result.ContinueWith(res =>  {
 
                var httpResult = res.Result;
                httpResult.EnsureSuccessStatusCode(); 
                httpResult.Content.ReadAsStreamAsync()
                    .ContinueWith(HandleSuccess, 
                    TaskContinuationOptions.NotOnFaulted)
                        .ContinueWith(ae => FireDownloadFailed(ae.Exception), 
                                      TaskContinuationOptions.OnlyOnFaulted);
 
            }).ContinueWith(ae => FireDownloadFailed(ae.Exception), TaskContinuationOptions.OnlyOnFaulted);
 
        }
 
        private void HandleSuccess(Task<Stream> result)
        {
            try
            {
                var fileService = MvxFileStoreHelper.SafeGetFileStore();
                var tempFilePath = DownloadPath + ".tmp";
 
                using (result.Result)
                {
                    fileService.WriteFile(tempFilePath,
                                          (fileStream) =>
                    {
                        result.Result.CopyTo(fileStream);
                    });
                }
                fileService.TryMove(tempFilePath, DownloadPath, true);
            }
            catch (Exception exception)
            {
                FireDownloadFailed(exception);
                return;
            }
 
            FireDownloadComplete();
        }
 
        private void FireDownloadFailed(Exception exception)
        {
            var handler = DownloadFailed;
            if (handler != null)
                handler(this, new MvxValueEventArgs<Exception>(exception));
        }
 
        private void FireDownloadComplete()
        {
            var handler = DownloadComplete;
            if (handler != null)
                handler(this, new MvxFileDownloadedEventArgs(Url, DownloadPath));
        }
    }
}

4 Responses

Leave a Reply to Cheesebaron Cancel reply