Get started

In this tutorial, we build an app for two phones. Each phone shows the other phone in a list. A tap on a row sends a line of text to the other screen. The tutorial takes about ten minutes plus build time.

The finished app is samples/Minimal. Every snippet on this page comes from that app.

You need a .NET MAUI app and two physical devices, both Android or both iPhone. An Android phone and an iPhone never discover each other, because the two platforms use unrelated native transports. See why the platforms do not interoperate.

Your app's SupportedOSPlatformVersion must be at least Android 24 and iOS 26.0. To start from an empty app, run dotnet new maui -n Minimal.

1. Install the package

dotnet add package Plugin.Maui.NearbyConnectivity

2. Register the plugin

In MauiProgram.cs, register the plugin with a ServiceId. Both devices must use the same string, because only devices with the same ServiceId find each other.

builder.AddNearbyConnectivity(options =>
{
    options.ServiceId = "nearbyminimal";
});

Build and run the app. You should see the app start as before. The plugin starts nothing yet.

3. Add the platform entries

On Android, the manifest needs no changes, because the package declares every permission that Nearby Connections needs. If your app does not derive the user's physical location from Bluetooth or Wi-Fi scan results, add this block:

<uses-permission
  android:name="android.permission.BLUETOOTH_SCAN"
  android:usesPermissionFlags="neverForLocation" />
<uses-permission
  android:name="android.permission.NEARBY_WIFI_DEVICES"
  android:usesPermissionFlags="neverForLocation" />

On iOS, add two Bonjour entries and a usage string to Platforms/iOS/Info.plist:

<key>NSBonjourServices</key>
<array>
    <string>_nearbyminimal._tcp</string>
    <string>_nearbyminimal._udp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Finds nearby devices running this app so they can exchange messages.</string>

For every entry and its purpose, see Set up Android and iOS.

4. Request permission and start

Request NearbyPermission before you start. The plugin never shows the permission prompt itself. On Android, the start methods throw NearbyException when a permission is missing.

Start both advertising and discovery. Advertising makes this device visible. Discovery finds the other devices. The start methods throw NearbyException when the device cannot start, for example when Bluetooth is off. Catch it: an unhandled exception in an async void method ends the app.

protected override async void OnAppearing()
{
    base.OnAppearing();

    var status = await Permissions.RequestAsync<NearbyPermission>();
    if (status != PermissionStatus.Granted)
    {
        _log.Text = "Permission denied.";
        return;
    }

    // Accept every incoming request. A real app prompts the user instead.
    _nearby.Advertiser.SetConsent(NearbyConsentPolicy.AcceptAll);

    try
    {
        await _nearby.Advertiser.StartAsync();
        await _nearby.Discoverer.StartAsync();
    }
    catch (NearbyException ex)
    {
        _log.Text = $"Could not start: {ex.Message}";
        return;
    }

    _receiving = new CancellationTokenSource();
    _ = ReceiveAsync(_receiving.Token);
}
Important

SetConsent(NearbyConsentPolicy.AcceptAll) accepts every request from any device that knows the ServiceId. This policy keeps the tutorial short. In a shipping app, leave the policy at Hold and let a person decide. See Answer an inbound request.

You should see the permission prompt on the first run.

5. Send a message

Connect to the tapped device. Then send the text as UTF-8 bytes. ConnectAsync returns the existing connection when the device is already connected, so a second tap is not an error.

async void OnDeviceSelected(object? sender, SelectionChangedEventArgs e)
{
    if (e.CurrentSelection is not [NearbyDevice device])
    {
        return;
    }

    _deviceList.SelectedItem = null;

    try
    {
        var connection = await device.ConnectAsync();
        await connection.SendAsync(Encoding.UTF8.GetBytes(_message.Text));
        Write($"Sent to {device.Presence.DisplayName}");
    }
    catch (NearbyConnectException ex)
    {
        Write($"Could not connect: {ex.Reason}");
    }
    catch (NearbyException ex)
    {
        Write($"Could not send: {ex.Message}");
    }
}

The device list is a NearbyDeviceCollection. It handles the plugin's DeviceChanged event and raises INotifyCollectionChanged, so a bound CollectionView updates without more code.

6. Receive messages

Read every inbound payload in one loop. PayloadsAsync merges the payloads from every connection, so you do not track the connection that each payload came from.

async Task ReceiveAsync(CancellationToken cancellationToken)
{
    await foreach (var (from, payload) in _nearby.PayloadsAsync(cancellationToken))
    {
        using (payload)
        {
            if (payload is NearbyBytesPayload bytes)
            {
                var text = Encoding.UTF8.GetString(bytes.Data.Span);
                Write($"{from.Presence.DisplayName}: {text}");
            }
        }
    }
}

Payloads arrive on a thread-pool thread, never on the UI thread. Dispatch to the UI thread before you touch a control. You own every payload, so dispose each one when you are done with it.

7. Stop when the page disappears

Stop the plugin in OnDisappearing:

protected override async void OnDisappearing()
{
    base.OnDisappearing();

    _receiving?.Cancel();
    await _nearby.StopAllAsync();
}

StopAllAsync stops advertising, stops discovery, and disconnects every peer. The plugin stays usable, so you can start again later.

8. Run it on two devices

Deploy the app to both phones. Start the app on each phone.

dotnet build -t:Run -f net10.0-android

Within a few seconds, you should see these results:

  1. Each phone lists the other phone by name.
  2. A tap on a row on one phone connects the two phones.
  3. The message appears on the other phone, prefixed with the sender's name.

If the list stays empty, see Troubleshooting. The two most common causes are a ServiceId that differs between the two apps and a missing Android runtime permission.

Next