Live CODE |

Deeplinking в Xamarin

Deep Linking или App Links - способ указать приложению что его хотят открыть на определенной странице.

Пример такой ссылки mydeeplinkscheme://mydeeplinkhost/path?arg1=value1

iOS

Документация

Для того что бы зарегистрировать приложение на схему нужно в Info.plist добавить

<dict>
    . . .
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleURLSchemes</key>
            <array>
                <string>mydeeplinkscheme</string>
            </array>
            <key>CFBundleURLName</key>
            <string>My Deeplink Scheme</string>
        </dict>
    </array>
</dict>

Как правильно заполнять CFBundleURLName вопрос открытый, в документации написано следующее:

A string containing the abstract name of the URL scheme. To ensure uniqueness, it is recommended that you specify a reverse-DNS style of identifier, for example, com.acme.myscheme. The string you specify is also used as a key in your app’s InfoPlist.strings file. The value of the key is the human-readable scheme name.

В приложении ссылку можно получить в двух случаях: при старте приложения по глубокой ссылке в методе FinishedLaunching, а если приложение уже запущено тогда в HandleOpenURL.

public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
    NSObject urlObject;
    if (options.TryGetValue(UIApplication.LaunchOptionsUrlKey, out urlObject))
    {
        var url = urlObject as NSUrl;
        if (url != null)
        {
            OpenDeepLink(url);
        }
    }
}
public override bool HandleOpenURL (UIApplication application, NSUrl url)
{
    OpenDeepLink(url);
    return true;
}
void OpenDeepLink(NSUrl deepLink)
{
    . . .
}

После тестирования я отказался от обработки ссылки в методе FinishedLaunching, у меня HandleOpenURL вызывался даже если приложение не было запущено.

Android

Документация

Нужно в AndroidManifest.xml добавить следующее

<activity android:name="com.package.name.DeeplinkingActivity">
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="mydeeplinkscheme" />
    <data android:host="mydeeplinkhost" />  <!-- можно без этой строки -->
  </intent-filter>
</activity>

Если хост не указывать тогда ссылки не будут фильтроваться по хосту. Указывать хост может быть полезно для случая когда мы хотим подписаться на популятную схему, например http. Скажем есть каталог сайт и приложение, с одинаковым содержимым. Тогда открывая на сайт в браузере выскочит предложение открыть приложение.

В Xamarin правильно заполнить манифест проще всего через аттрибуты. Фильтров может быть несколько.

[Activity()]
[IntentFilter(new[] { Android.Content.Intent.ActionView },
    DataScheme = "mydeeplinkscheme",
    Categories = new[] {
        Android.Content.Intent.CategoryDefault,
        Android.Content.Intent.CategoryBrowsable
    })]
[IntentFilter(new[] { Android.Content.Intent.ActionView },
    DataScheme = "http",
    DataHost = "mycatalog.com",
    Categories = new[] {
        Android.Content.Intent.CategoryDefault,
        Android.Content.Intent.CategoryBrowsable
    })]
class DeeplinkingActivity { . . . }

Я использую отдельное активити для получения ссылок в котором решаю что запускать следующее, но можно для каждой схемы/хоста использовать свою активити.

Ссылка приходит в метод OnCreate указанного активити, в моем примере DeeplinkingActivity

override protected void OnCreate(Bundle savedInstanceState)
{
    base.OnCreate(savedInstanceState);
    if (Intent != null && Intent.Data != null)
    {
        OpenDeepLink(Intent.Data);
    }
    Finish();
}
void OpenDeepLink(Android.Net.Uri deepLink)
{
    . . .
}

Еще один раздел

Такие ссылки работают в стандартном браузере если они расположены на странице и по ним нажать пальцем. Если ссылку ввести в адресную строку, то браузер открывает поисковик с этой строкой. Также если такую ссылку послать по почте то она не подсвечивается (или может не подсвечаваться) даже если письмо отправлено как HTML.

Я решил эти мелочи добавлением HTTP прокси сервера, который делает редирект HTTP/1.1 302 Found Location: mydeeplinkscheme://blah/blah

А HTTP ссылку уже можно вводить в адресную строку браузера и посылать по почте.

Вывод

Как видно использовать глубинные ссылки достаточно легко.

Разобрать ссылку и открыть нужную страницу это уже дело техники.

 

Best wishes
And happy coding!

Использование CancellationTokenSource

Библиотека параллельных задач (TPL) представляет собой набор открытых типов и API-интерфейсов в пространствах имен System.Threading и System.Threading.Tasks. Цель TPL — повышение производительности труда разработчиков за счет упрощения процедуры добавления параллелизма в приложения.

CancellationTokenSource (далее CTS) - объект для создания и управления токенами отмены (CancellationToken).

Обычный вариант использования выглядит так:

cts = new CancellationTokenSource();
var task = Task.Run(() => SomeWork(cts.Token), cts.Token);

. . . . .

cts.Cancel();

Я хочу рассказать о ситуации когда некоторый объект должен выполнять в единицу времени только одну задачу, например вывод результатов живого поиска, когда приходит запрос на вывод следующего результата предыдущий становится уже не актуальным. Простой пример решения такой задачи:

CancellationTokenSource сts = null;

async void OnTextChanged(string text)
{
  try
  {
    if (cts != null)
        cts.Cancel();
    cts = new CancellationTokenSource();

    var result = await DownloadSuggestionsAsync(text, cts.Token);
    ShowSuggestions(result);
  }
  catch(TaskCanceledException)
  {
  }
}

Такой вариант хорошо работает при написании пользовательского интерфейса когда все выполняется в одном потоке и частота вызова OnTextChanged низкая. А что если метод аналогичный этому вызывается не на реакцию действия пользователя, а на результат какого либо сервиса, и вызовы могут быть из разных потоков. Самым просты решение это добавить lock. Но использование lock приведет к кратковременной блокировке вызывающих потоков и усложнению кода, мы же не хотим чтоб загрузка данных также попала под синхронизацию. Также хотелось бы иметь простой читабельный код.

Решение

Для решения этой проблемы хочу обратить внимание на unique_ptr из C++. Это умный указатель который получает единоличное владение объектом и разрушает его, когда unique_ptr выходит из области видимости или ему присваивается новый объект.

В результате сказанного выше получаем пару методов для работы с CTS:

public static CancellationTokenSource NewCts(ref CancellationTokenSource cts,
                                             params CancellationToken[] tokens)
{
    var newCts = tokens.Length == 0 ? new CancellationTokenSource()
                                    : CancellationTokenSource.CreateLinkedTokenSource(tokens);
    var tmp = Interlocked.Exchange(ref cts, newCts);
    if (tmp != null)
        tmp.Cancel();
    return newCts;
}

public static void DeleteCts(ref CancellationTokenSource cts)
{
    var tmp = Interlocked.Exchange(ref cts, null);
    if (tmp != null)
        tmp.Cancel();
} 

Метод Interlocked.Exchange - атомарная операция которая меняет значения двух переменных. Это позволяет дать гарантию что при одновременном вызове NewCts из разных потоков все созданные экземпляры будут в состоянии отмены кроме одного, последнего вызвавшего Interlocked.Exchange.

Для работы нужна переменная в которой хранится активный CTS (в примере поле класса _cts), она передается по ссылке. Важно! NewCts возвращает новый созданный им CTS объект в то время как переданная переменная (в примере _cts) будет содержать последний активный CTS. Они могут отличаться при одновременном вызове из разных потоков.

CancellationTokenSource _cts = null;
async void OnTextChanged(string text)
{
  try
  {
    var token = Utils.NewCts(ref _cts).Token;
    var result = await DownloadSuggestionsAsync(text, token);
    ShowSuggestions(result);
  }
  catch(TaskCanceledException)
  {
  }
}

Метод NewCts также позволяет создавать связные CTS.

По окончанию работы или для принудительной отмены текущей задачи используется DeleteCts.

Достоинства:

  • удобная запись;
  • гарантия выполнения только одной операции в единицу времени при кратковременном спаме событий.

Недостатки:

  • не подходит если события спамят постоянно, тогда результат мы просто не увидим.

Best wishes
And happy coding!

Первый пост

Это первый пост.

Мое имя Александр и Я инди разаботчик.

Я пока не знаю зачем мне нужен этот блог, но может быть в процессе Я это узнаю.

Большой заголовок H2

Всякий текст на котором буду проверять стили сайта.

Заголовок поменьше H3

С правильнописанием на русском у меня не очень. Лучше всего у меня получается писать на C# или C++, хотя принципиальной разницы на чем писать я не вижу.

	// Это тестовый кусок кода 
	foreach (var tmp in list)
	{
		Console.WriteLine("tmp: " + tmp);
	}
	
	// в теге <pre> выглядит куда лучше

Best wishes
And happy coding!