Xamarin Guide — .NET Cross-Platform Mobile Development
Xamarin is Microsoft’s cross-platform mobile development framework that lets you build native iOS, Android, and Windows apps with .NET and C#, sharing an average of 75-90% of code across platforms through a single language and framework.
What You’ll Learn
You’ll create UIs with XAML in Xamarin.Forms, implement the MVVM pattern for clean separation of concerns, write platform-specific code when needed, access native device features through Xamarin.Essentials, and target iOS, Android, and Windows from a shared codebase.
Why Xamarin Matters
Xamarin gives .NET developers a path to mobile without learning Java, Kotlin, Swift, or Objective-C. It compiles to native code — unlike hybrid approaches — delivering native performance and native UI access. DodaTech’s internal asset management system uses Xamarin because the team already knows C# and .NET, and the app needs native Bluetooth and NFC access that only Xamarin’s platform-specific APIs can provide efficiently.
Xamarin Learning Path
flowchart LR
A[C# and .NET] --> B[Xamarin]
B --> C[Xamarin.Forms]
C --> D[XAML UI]
C --> E[MVVM Pattern]
C --> F[Xamarin.Essentials]
F --> G[Device APIs]
D --> H[Native Rendering]
B:::current
classDef current fill:#3498DB,color:#fff,stroke:#333,stroke-width:2px
Xamarin Architecture
Xamarin apps share business logic through a .NET Standard library while platform-specific projects handle UI and native API calls:
Xamarin Solution
├── DodaTechApp (Shared) # .NET Standard library
│ ├── Models/ # Data models
│ ├── ViewModels/ # MVVM view models
│ ├── Services/ # Business logic
│ └── Views/ # XAML pages (Forms)
├── DodaTechApp.Android/ # Android-specific
│ ├── MainActivity.cs
│ └── Resources/
├── DodaTechApp.iOS/ # iOS-specific
│ ├── AppDelegate.cs
│ └── Info.plist
└── DodaTechApp.UWP/ # Windows-specific
├── App.xaml
└── Package.appxmanifestCreating a Xamarin.Forms App
# Install Xamarin workload (if not already)
dotnet workload install maui
# Create a new Xamarin.Forms project
dotnet new maui -n DodaTechApp
cd DodaTechApp
dotnet buildXAML UI — Declarative Layouts
XAML (eXtensible Application Markup Language) describes the UI declaratively:
<!-- MainPage.xaml -->
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="DodaTechApp.MainPage"
Title="DodaTech Scanner">
<ContentPage.Resources>
<ResourceDictionary>
<Color x:Key="PrimaryColor">#2c3e50</Color>
<Color x:Key="AccentColor">#3498db</Color>
</ResourceDictionary>
</ContentPage.Resources>
<Grid Padding="20" RowDefinitions="Auto,*,Auto">
<!-- Header -->
<StackLayout Grid.Row="0" Spacing="10" Margin="0,20,0,20">
<Label Text="Device Scanner"
FontSize="28"
FontAttributes="Bold"
TextColor="{StaticResource PrimaryColor}" />
<Label Text="Scan nearby devices and view details"
FontSize="14"
TextColor="Gray" />
</StackLayout>
<!-- Device List -->
<CollectionView Grid.Row="1" x:Name="DeviceList"
SelectionMode="Single"
SelectionChanged="OnDeviceSelected">
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame Padding="15" Margin="0,5" HasShadow="True"
BorderColor="LightGray" CornerRadius="8">
<Grid ColumnDefinitions="Auto,*,Auto">
<BoxView Grid.Column="0"
WidthRequest="10" HeightRequest="10"
CornerRadius="5"
Color="{Binding StatusColor}" />
<StackLayout Grid.Column="1" Spacing="3" Margin="10,0">
<Label Text="{Binding Name}" FontSize="16" FontAttributes="Bold" />
<Label Text="{Binding Type}" FontSize="12" TextColor="Gray" />
</StackLayout>
<Label Grid.Column="2"
Text="{Binding SignalStrength, StringFormat='{0} dBm'}"
FontSize="12" VerticalOptions="Center" />
</Grid>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- Scan Button -->
<Button Grid.Row="2"
Text="Start Scan"
BackgroundColor="{StaticResource AccentColor}"
TextColor="White"
FontSize="18"
Margin="0,20"
HeightRequest="50"
CornerRadius="25"
Clicked="OnStartScan" />
</Grid>
</ContentPage>MVVM Pattern in Practice
MVVM (Model-View-ViewModel) separates UI (View) from business logic (ViewModel) using data binding:
// Models/DeviceInfo.cs
public class DeviceInfo : INotifyPropertyChanged
{
private string name;
private string type;
private int signalStrength;
private bool isConnected;
public string Name
{
get => name;
set { name = value; OnPropertyChanged(); }
}
public string Type
{
get => type;
set { type = value; OnPropertyChanged(); }
}
public int SignalStrength
{
get => signalStrength;
set { signalStrength = value; OnPropertyChanged(); }
}
public bool IsConnected
{
get => isConnected;
set { isConnected = value; OnPropertyChanged(); }
}
public Color StatusColor => IsConnected ? Colors.Green : Colors.Gray;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}// ViewModels/MainViewModel.cs
public class MainViewModel : INotifyPropertyChanged
{
private ObservableCollection<DeviceInfo> devices;
private bool isScanning;
private string statusMessage;
public MainViewModel()
{
devices = new ObservableCollection<DeviceInfo>();
StartScanCommand = new Command(async () => await StartScanAsync());
}
public ObservableCollection<DeviceInfo> Devices
{
get => devices;
set { devices = value; OnPropertyChanged(); }
}
public bool IsScanning
{
get => isScanning;
set { isScanning = value; OnPropertyChanged(); OnPropertyChanged(nameof(IsNotScanning)); }
}
public bool IsNotScanning => !IsScanning;
public string StatusMessage
{
get => statusMessage;
set { statusMessage = value; OnPropertyChanged(); }
}
public ICommand StartScanCommand { get; }
private async Task StartScanAsync()
{
IsScanning = true;
StatusMessage = "Scanning for devices...";
Devices.Clear();
// Simulate device discovery
var foundDevices = new List<DeviceInfo>
{
new DeviceInfo { Name = "DodaHub-01", Type = "IoT Gateway", SignalStrength = -42, IsConnected = true },
new DeviceInfo { Name = "DurgaSensor-A3", Type = "Security Sensor", SignalStrength = -58, IsConnected = true },
new DeviceInfo { Name = "DodaCam-Pro", Type = "IP Camera", SignalStrength = -73, IsConnected = false },
new DeviceInfo { Name = "DodaBridge-X1", Type = "Network Bridge", SignalStrength = -65, IsConnected = true }
};
// Simulate gradual discovery
foreach (var device in foundDevices)
{
await Task.Delay(500);
Devices.Add(device);
}
IsScanning = false;
StatusMessage = $"Found {Devices.Count} device(s)";
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}// MainPage.xaml.cs — Code-behind
public partial class MainPage : ContentPage
{
private readonly MainViewModel viewModel;
public MainPage()
{
InitializeComponent();
viewModel = new MainViewModel();
BindingContext = viewModel;
}
private async void OnStartScan(object sender, EventArgs e)
{
await viewModel.StartScanCommand.ExecuteAsync(null);
}
private async void OnDeviceSelected(object sender, SelectionChangedEventArgs e)
{
if (e.CurrentSelection.FirstOrDefault() is DeviceInfo selectedDevice)
{
await DisplayAlert("Device Details",
$"Name: {selectedDevice.Name}\n" +
$"Type: {selectedDevice.Type}\n" +
$"Signal: {selectedDevice.SignalStrength} dBm\n" +
$"Connected: {selectedDevice.IsConnected}",
"OK");
}
}
}Output: The scanner screen displays a header, a collection of device cards (each showing name, type, signal strength, and connection status via a colored dot), and a scan button. Tapping “Start Scan” populates the list progressively. Selecting a device shows its details in an alert dialog.
Xamarin.Essentials — Unified Native API Access
Xamarin.Essentials provides a single API for common native features across platforms:
using Xamarin.Essentials;
public class DeviceService
{
// Check connectivity
public bool IsConnected =>
Connectivity.NetworkAccess == NetworkAccess.Internet;
// Get device info
public string GetDeviceInfo() =>
$"{DeviceInfo.Model} ({DeviceInfo.Platform}) - {DeviceInfo.VersionString}";
// Get current location
public async Task<(double Latitude, double Longitude)> GetLocationAsync()
{
try
{
var request = new GeolocationRequest(GeolocationAccuracy.High);
var location = await Geolocation.GetLocationAsync(request);
if (location != null)
return (location.Latitude, location.Longitude);
}
catch (FeatureNotSupportedException)
{
// GPS not supported on this device
}
catch (PermissionException)
{
// Permission not granted
}
return (0, 0);
}
// Vibrate the device
public void VibrateDevice(int durationMs = 500)
{
try
{
Vibration.Vibrate(TimeSpan.FromMilliseconds(durationMs));
}
catch (FeatureNotSupportedException) { }
}
// Share text
public async Task ShareTextAsync(string text, string title)
{
await Share.RequestAsync(new ShareTextRequest
{
Text = text,
Title = title
});
}
// Secure storage
public async Task SaveSecureAsync(string key, string value)
{
await SecureStorage.SetAsync(key, value);
}
public async Task<string> GetSecureAsync(string key)
{
return await SecureStorage.GetAsync(key);
}
}Output: These methods work identically on iOS, Android, and Windows. GetLocationAsync returns GPS coordinates with high accuracy on all platforms. VibrateDevice triggers haptic feedback. SaveSecureAsync stores data in the platform’s secure keystore (Keychain on iOS, EncryptedSharedPreferences on Android).
Platform-Specific Code
When you need truly native functionality, use conditional compilation or partial classes:
// Services/FileService.cs — Shared
public partial class FileService
{
public partial string GetAppDataPath();
public async Task<string> ReadFileAsync(string filename)
{
var path = Path.Combine(GetAppDataPath(), filename);
return await File.ReadAllTextAsync(path);
}
}// Services/FileService.android.cs — Android implementation
public partial class FileService
{
public partial string GetAppDataPath()
{
return Android.App.Application.Context.FilesDir.AbsolutePath;
}
}// Services/FileService.ios.cs — iOS implementation
public partial class FileService
{
public partial string GetAppDataPath()
{
var documents = Environment.GetFolderPath(
Environment.SpecialFolder.MyDocuments);
return Path.Combine(documents, "..", "Library", "AppData");
}
}Output: The shared code calls GetAppDataPath() without knowing the platform. At compile time, the correct partial class implementation is included for the target platform. This pattern keeps platform-specific code isolated while maintaining a clean shared API.
Common Mistakes Beginners Make
Not using data binding: Manipulating UI elements directly from code-behind defeats MVVM and makes testing harder. Use
{Binding}in XAML and update ViewModel properties.Forgetting
OnPropertyChanged: Without callingOnPropertyChanged, the UI won’t reflect property changes. Use[CallerMemberName]to avoid typos in property name strings.Blocking the UI thread: Network calls and file operations must use
async/await. Blocking the main thread causes ANR (Android) or app hanging (iOS).Assuming identical behavior across platforms: iOS and Android handle navigation, back buttons, and keyboard differently. Test on both platforms and use platform-specific code where needed.
Not handling lifecycle events: Mobile apps are suspended, resumed, and terminated frequently. Override
OnAppearing,OnDisappearing, andSleep/Resumemethods to manage state.Missing permission checks: Android 6+ requires runtime permissions for camera, location, and storage. Xamarin.Essentials
PermissionsAPI handles this, but you must check before accessing features.
Practice Questions
- What is the purpose of the ViewModel in MVVM?
- How does Xamarin.Forms share code between platforms?
- What is Xamarin.Essentials used for?
- How do you write platform-specific code in a Xamarin app?
- What is the difference between
PropertyChangedandCommand?
Answers:
- The ViewModel holds the presentation logic and state, exposing properties and commands that the View binds to. It keeps the View clean and makes the logic testable without a UI.
- Through a .NET Standard library containing shared models, view models, services, and XAML Forms pages. Platform-specific projects reference the shared library.
- It provides a unified API for device features like GPS, connectivity, secure storage, text-to-speech, and sensors, abstracting platform differences.
- Use
#if __ANDROID__/#if __IOS__conditional compilation, or partial classes with platform-specific files. PropertyChangednotifies the UI that a property value has changed.Commandbinds user actions (button taps) to ViewModel methods, enabling separation of event handling.
Challenge
Build a weather app with Xamarin.Forms: use Xamarin.Essentials for GPS location, fetch weather data from a public API, display a 5-day forecast with a CollectionView, support pull-to-refresh, and use MVVM throughout. Add platform-specific battery optimization handling.
Real-World Task
Create a field data collection app for inspections: use Xamarin.Forms with Shell navigation, implement photo capture with platform-specific camera code, store data in SQLite, sync to a REST API when connectivity is available (using Xamarin.Essentials Connectivity API), and handle offline scenarios.
FAQ
Try It Yourself
dotnet new maui -n DodaTechApp
cd DodaTechApp
dotnet buildReplace the default counter with the device scanner UI from this tutorial. Add a second page for device details with navigation using Shell routing.
What’s Next
Related topics: C#, .NET, XAML, Flutter
What’s Next
Congratulations on completing this Xamarin tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro