Mobiilisovelluksen kehityksessä tiedon tallentaminen paikallisesti on yksi niistä asioista, joita teet käytännössä joka projektissa. Joskus tarvitset kevyen tavan muistaa käyttäjän valintoja — teema, kieli, viimeisin näkymä. Toisinaan taas käsissäsi on arkaluonteista dataa, kuten autentikointitokeneja tai API-avaimia, joita ei todellakaan kannata jättää selkotekstinä lojumaan. .NET MAUI tarjoaa näihin kahteen tarpeeseen omat ratkaisunsa: Preferences ja SecureStorage.
Tässä oppaassa käymme läpi molemmat API:t käytännön esimerkkien avulla, tutkimme alustakohtaisia toteutuksia ja rakennamme MVVM-arkkitehtuurin mukaisen asetuspalvelun, joka yhdistää nämä molemmat mekanismit. Jos olet seurannut aiempia oppaitamme SQLite-tietokannasta ja REST API -integraatiosta, tämä artikkeli täydentää tiedonhallinnan kokonaiskuvaa.
Milloin käyttää Preferencesiä, SecureStoragea vai SQLitea?
Ennen kuin hyppäät koodiin, on hyvä hahmottaa milloin mikäkin tallennusmekanismi on oikea valinta:
- Preferences — Pienille, ei-arkaluonteisille avain-arvo -pareille. Esimerkkejä: teema-asetus (tumma/vaalea), kielivalinta, viimeisin käytetty välilehti, käyttäjän hyväksymät ehdot.
- SecureStorage — Arkaluonteiselle datalle, joka vaatii salauksen. Esimerkkejä: OAuth-tokenit, API-avaimet, refresh tokenit, käyttäjän salasanan hash.
- SQLite — Rakenteiselle datalle, joka sisältää useita tietueita ja vaatii kyselyominaisuuksia. Esimerkkejä: tuotelistaukset, käyttäjäprofiilitiedot, offline-välimuisti.
Pääsääntö on yksinkertainen: pieni ja yksinkertainen avain-arvo -pari → Preferences. Arkaluonteista → SecureStorage. Tarvitset taulukkorakennetta ja kyselyjä → SQLite. Siinä se.
Preferences API käytännössä
IPreferences-rajapinta tarjoaa synkronisen API:n avain-arvo -parien tallentamiseen. Se on osa Microsoft.Maui.Storage-nimiavaruutta ja käyttää alustakohtaisia tallennusmekanismeja kulissien takana.
Tuetut tietotyypit
Preferences tukee seuraavia tietotyyppejä arvoina:
stringintlongdoublefloatboolDateTime
Mainittakoon, että DateTime-arvot tallennetaan 64-bittisessä binäärimuodossa ToBinary- ja FromBinary-metodeilla. Pieni varoitus: ei-UTC-muotoisten ajanhetkien kohdalla konversio saattaa muuttaa arvoa hieman, joten käytä mieluiten aina DateTime.UtcNow.
Asetusten tallentaminen ja lukeminen
using Microsoft.Maui.Storage;
// Tallenna erilaisia arvoja
Preferences.Default.Set("theme", "dark");
Preferences.Default.Set("font_size", 16);
Preferences.Default.Set("notifications_enabled", true);
Preferences.Default.Set("last_sync", DateTime.UtcNow);
// Lue arvoja — toinen parametri on oletusarvo, jos avainta ei löydy
string theme = Preferences.Default.Get("theme", "light");
int fontSize = Preferences.Default.Get("font_size", 14);
bool notificationsEnabled = Preferences.Default.Get("notifications_enabled", true);
DateTime lastSync = Preferences.Default.Get("last_sync", DateTime.MinValue);
Get-metodi palauttaa aina joko tallennetun arvon tai antamasi oletusarvon. Se ei koskaan heitä poikkeusta puuttuvan avaimen takia — mikä on rehellisesti sanottuna mukavan huoleton API-ratkaisu.
Avaimen olemassaolon tarkistus ja poisto
// Tarkista onko avain olemassa
bool hasTheme = Preferences.Default.ContainsKey("theme");
// Poista yksittäinen avain
Preferences.Default.Remove("theme");
// Poista kaikki tallennetut asetukset
Preferences.Default.Clear();
ContainsKey on hyödyllinen tilanteissa, joissa oletusarvo voi olla sama kuin tallennettu arvo. Tällöin et voi pelkästään Get-metodin palauttaman arvon perusteella päätellä, onko asetus jo tallennettu vai ei.
Alustakohtaiset tallennusmekanismit
Preferences käyttää kullakin alustalla natiivia tallennusratkaisua:
- Android —
SharedPreferences. Tiedot säilyvät sovelluksen uudelleenkäynnistämisen yli, mutta poistuvat sovelluksen poiston yhteydessä (paitsi jos Android Auto Backup on käytössä — lisää tästä myöhemmin). - iOS / Mac Catalyst —
NSUserDefaults. Tuttu mekanismi iOS-kehittäjille. - Windows —
ApplicationDataContainer(LocalSettings). Tässä kannattaa huomata rajoitukset: avaimen nimen enimmäispituus on 255 merkkiä, yksittäisen asetuksen koko enintään 8 kt ja yhdistelmäasetuksen koko enintään 64 kt.
SecureStorage API käytännössä
ISecureStorage-rajapinta tarjoaa asynkronisen API:n salattujen avain-arvo -parien hallintaan. Toisin kuin Preferences, SecureStorage salaa sekä tallennetun datan että (Android-alustalla) myös avaimet.
Tämä on se rajapinta, jota käytät kun data on oikeasti herkkää.
Arvojen tallentaminen ja lukeminen
using Microsoft.Maui.Storage;
// Tallenna arkaluonteinen arvo
await SecureStorage.Default.SetAsync("oauth_token", "eyJhbGciOiJSUzI1NiIs...");
await SecureStorage.Default.SetAsync("refresh_token", "dGhpcyBpcyBhIHJlZn...");
await SecureStorage.Default.SetAsync("api_key", "sk-proj-abc123def456");
// Lue arvo — palauttaa null jos avainta ei löydy
string? oauthToken = await SecureStorage.Default.GetAsync("oauth_token");
if (oauthToken is null)
{
// Käyttäjä ei ole kirjautunut sisään tai token on poistettu
await NavigateToLoginAsync();
}
Tärkeä ero Preferencesiin: GetAsync palauttaa null kun avainta ei löydy — ei oletusarvoa. Null-tarkistus on siis aina pakollinen.
Arvojen poistaminen
// Poista yksittäinen avain — palauttaa true jos poisto onnistui
bool removed = SecureStorage.Default.Remove("oauth_token");
// Poista kaikki salatut arvot (esim. uloskirjautumisen yhteydessä)
SecureStorage.Default.RemoveAll();
Virheenkäsittely
Tässä kohtaa SecureStorage vaatii hieman enemmän huomiota kuin Preferences. Se voi heittää poikkeuksia useissa tilanteissa: laite ei tue salattua tallennusta, salausavaimet ovat muuttuneet tai data on korruptoitunut. Käytännössä tämä tarkoittaa, että kutsut kannattaa aina kääriä try-catch-lohkoon:
try
{
await SecureStorage.Default.SetAsync("oauth_token", token);
}
catch (Exception ex)
{
// Tallennuksen salaus epäonnistui
// Voi johtua laitteen rajoituksista tai korruptoituneesta keystoresta
System.Diagnostics.Debug.WriteLine($"SecureStorage virhe: {ex.Message}");
// Viimeisenä keinona: tyhjennä ja yritä uudelleen
SecureStorage.Default.RemoveAll();
await SecureStorage.Default.SetAsync("oauth_token", token);
}
Olen itse törmännyt tähän erityisesti vanhemmilla Android-laitteilla, joissa Keystore käyttäytyy joskus arvaamattomasti. Hyvä virheenkäsittely säästää paljon päänvaivaa tuotannossa.
Alustakohtainen salaus
SecureStorage käyttää kullakin alustalla parasta mahdollista salausmekanismia:
- Android —
EncryptedSharedPreferencesAndroid Security -kirjastosta. Avaimet salataan deterministisesti ja arvot ei-deterministisesti AES-256 GCM -algoritmilla. Tämä on Googlen oma suositus arkaluonteisen datan käsittelyyn. - iOS / Mac Catalyst — Keychain. iOS:n sisäänrakennettu turvallinen tallennusmekanismi, jota käyttävät myös järjestelmän omat sovellukset. Hyvä tietää: Keychain-data voi synkronoitua iCloud-palveluun, joten sovelluksen poistaminen ei välttämättä poista arvoja käyttäjän laitteelta.
- Windows —
DataProtectionProvider. Pakatuissa sovelluksissa salatut arvot tallennetaanLocalSettings-säilöön, pakkaamattomissa sovelluksissasecurestorage.dat-tiedostoon JSON-muodossa.
Android Auto Backup -ongelma ja sen ratkaisu
Tämä on yksi niistä asioista, jotka voivat aiheuttaa todella hämmentäviä bugeja jos et ole niistä tietoinen.
Android 6.0:sta (API-taso 23) lähtien Android varmuuskopioi automaattisesti sovelluksen datan, mukaan lukien SharedPreferences-tiedostot. Ongelma on tämä: kun sovellus asennetaan uudelleen tai uudelle laitteelle, varmuuskopioidut salatut arvot palautetaan — mutta uudella asennuksella on eri salausavaimet. Lopputulos? Arvojen purku epäonnistuu.
.NET MAUI käsittelee tämän automaattisesti poistamalla ongelmallisen avaimen, mutta voit myös hallita tilannetta itse. Paras käytäntö on sulkea SecureStorage-tiedostot varmuuskopioinnin ulkopuolelle.
Vaihtoehto 1: Valikoiva varmuuskopiointi
Luo auto_backup_rules.xml-tiedosto Platforms/Android/Resources/xml/-kansioon:
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref"
path="${applicationId}.microsoft.maui.essentials.preferences.xml"/>
</full-backup-content>
Ja viittaa siihen AndroidManifest.xml-tiedostossa:
<application android:fullBackupContent="@xml/auto_backup_rules" ...>
</application>
Vaihtoehto 2: Auto Backup kokonaan pois
<application android:allowBackup="false" ...>
</application>
Tämä estää kaiken automaattisen varmuuskopioinnin. Käytä tätä vaihtoehtoa vain jos hallitset datan palautuksen muulla tavalla (tai jos sovelluksesi ei tarvitse varmuuskopiointia).
iOS-simulaattorin erityisasetus
Nopea huomio iOS-kehittäjille: simulaattorilla kehitettäessä SecureStorage vaatii Keychain-oikeuden (entitlement) käyttöönoton. Ilman sitä saat ajonaikaisen virheen, joka voi olla turhauttava jos et tiedä mistä on kyse.
Avaa tai luo projektin Entitlements.plist-tiedosto ja ota Keychain-oikeus käyttöön — sovelluksen bundle-tunniste lisätään automaattisesti käyttöryhmäksi. Fyysisellä laitteella tätä oikeutta ei tarvita, ja se tulisi poistaa tuotantobuildeista.
MVVM-asetuspalvelu: Preferences ja SecureStorage yhdessä
Okei, nyt päästään siihen mielenkiintoiseen osaan. Käytännön sovelluksessa haluat yleensä abstrahoida tallennuslogiikan palveluluokkaan, joka injektoidaan ViewModeleihin riippuvuusinjektion kautta. Tämä tekee koodista testattavaa ja noudattaa MVVM-arkkitehtuurin periaatteita.
Rajapinnan määrittely
public interface ISettingsService
{
// Preferences (ei-arkaluonteiset)
string Theme { get; set; }
int FontSize { get; set; }
bool NotificationsEnabled { get; set; }
DateTime LastSyncTime { get; set; }
// SecureStorage (arkaluonteiset)
Task<string?> GetAccessTokenAsync();
Task SetAccessTokenAsync(string token);
Task<string?> GetRefreshTokenAsync();
Task SetRefreshTokenAsync(string token);
Task ClearAuthDataAsync();
}
Rajapinta erottaa selkeästi synkroniset Preferences-operaatiot ja asynkroniset SecureStorage-operaatiot. Tämä jako ei ole sattumanvarainen — se heijastaa alla olevien API:den todellista luonnetta.
Palvelun toteutus
public class SettingsService : ISettingsService
{
private readonly IPreferences _preferences;
private readonly ISecureStorage _secureStorage;
public SettingsService(IPreferences preferences, ISecureStorage secureStorage)
{
_preferences = preferences;
_secureStorage = secureStorage;
}
// --- Preferences-pohjaiset asetukset ---
public string Theme
{
get => _preferences.Get("theme", "light");
set => _preferences.Set("theme", value);
}
public int FontSize
{
get => _preferences.Get("font_size", 14);
set => _preferences.Set("font_size", value);
}
public bool NotificationsEnabled
{
get => _preferences.Get("notifications_enabled", true);
set => _preferences.Set("notifications_enabled", value);
}
public DateTime LastSyncTime
{
get => _preferences.Get("last_sync", DateTime.MinValue);
set => _preferences.Set("last_sync", value);
}
// --- SecureStorage-pohjaiset arkaluonteiset tiedot ---
public async Task<string?> GetAccessTokenAsync()
=> await _secureStorage.GetAsync("access_token");
public async Task SetAccessTokenAsync(string token)
=> await _secureStorage.SetAsync("access_token", token);
public async Task<string?> GetRefreshTokenAsync()
=> await _secureStorage.GetAsync("refresh_token");
public async Task SetRefreshTokenAsync(string token)
=> await _secureStorage.SetAsync("refresh_token", token);
public async Task ClearAuthDataAsync()
{
_secureStorage.Remove("access_token");
_secureStorage.Remove("refresh_token");
}
}
Rekisteröinti MauiProgram.cs:ssä
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// Palvelut
builder.Services.AddSingleton<ISettingsService, SettingsService>();
// ViewModelit
builder.Services.AddTransient<SettingsViewModel>();
// Sivut
builder.Services.AddTransient<SettingsPage>();
return builder.Build();
}
}
Huomionarvoinen yksityiskohta: IPreferences ja ISecureStorage ovat jo valmiiksi rekisteröity .NET MAUI:n DI-konttiin, joten niitä ei tarvitse erikseen lisätä. SettingsService rekisteröidään singletonina, koska asetukset ovat käytännössä globaalia sovelluksen tilaa.
ViewModel asetussivulle
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class SettingsViewModel : ObservableObject
{
private readonly ISettingsService _settingsService;
public SettingsViewModel(ISettingsService settingsService)
{
_settingsService = settingsService;
LoadSettings();
}
[ObservableProperty]
private string _selectedTheme = "light";
[ObservableProperty]
private int _fontSize = 14;
[ObservableProperty]
private bool _notificationsEnabled = true;
[ObservableProperty]
private bool _isLoggedIn;
private void LoadSettings()
{
SelectedTheme = _settingsService.Theme;
FontSize = _settingsService.FontSize;
NotificationsEnabled = _settingsService.NotificationsEnabled;
}
partial void OnSelectedThemeChanged(string value)
{
_settingsService.Theme = value;
ApplyTheme(value);
}
partial void OnFontSizeChanged(int value)
{
_settingsService.FontSize = value;
}
partial void OnNotificationsEnabledChanged(bool value)
{
_settingsService.NotificationsEnabled = value;
}
[RelayCommand]
private async Task CheckLoginStatusAsync()
{
var token = await _settingsService.GetAccessTokenAsync();
IsLoggedIn = token is not null;
}
[RelayCommand]
private async Task LogoutAsync()
{
await _settingsService.ClearAuthDataAsync();
IsLoggedIn = false;
}
private static void ApplyTheme(string theme)
{
if (Application.Current is null) return;
Application.Current.UserAppTheme = theme switch
{
"dark" => AppTheme.Dark,
"light" => AppTheme.Light,
_ => AppTheme.Unspecified
};
}
}
CommunityToolkit.Mvvm tekee tässä paljon raskasta työtä puolestamme. [ObservableProperty]-attribuutti generoi automaattisesti INotifyPropertyChanged-toteutuksen, ja partial void On...Changed -metodit reagoivat arvojen muutoksiin ilman manuaalista event-käsittelyä.
Käytännön esimerkki: Kirjautumistokenin hallinta
Yksi yleisimmistä käyttötapauksista SecureStoragelle on autentikointitokenien hallinta. Tässä on kokonainen esimerkki, joka näyttää tokenin tallentamisen kirjautumisen jälkeen ja sen käytön HTTP-kutsuissa:
public class AuthService
{
private readonly ISecureStorage _secureStorage;
private readonly HttpClient _httpClient;
public AuthService(ISecureStorage secureStorage, HttpClient httpClient)
{
_secureStorage = secureStorage;
_httpClient = httpClient;
}
public async Task<bool> LoginAsync(string username, string password)
{
var loginRequest = new { Username = username, Password = password };
var response = await _httpClient.PostAsJsonAsync("/api/auth/login", loginRequest);
if (!response.IsSuccessStatusCode)
return false;
var result = await response.Content.ReadFromJsonAsync<LoginResponse>();
if (result is null)
return false;
// Tallenna tokenit turvallisesti
await _secureStorage.SetAsync("access_token", result.AccessToken);
await _secureStorage.SetAsync("refresh_token", result.RefreshToken);
return true;
}
public async Task<string?> GetAuthenticatedHeaderAsync()
{
return await _secureStorage.GetAsync("access_token");
}
public async Task LogoutAsync()
{
_secureStorage.Remove("access_token");
_secureStorage.Remove("refresh_token");
}
}
public record LoginResponse(string AccessToken, string RefreshToken);
Tämä AuthService voidaan sitten injektoida ViewModeliin tai — mikä on mielestäni vielä elegantimpi ratkaisu — HTTP-viestin käsittelijään (DelegatingHandler), joka automaattisesti liittää tokenin kaikkiin lähteviin HTTP-pyyntöihin:
public class AuthHandler : DelegatingHandler
{
private readonly ISecureStorage _secureStorage;
public AuthHandler(ISecureStorage secureStorage)
{
_secureStorage = secureStorage;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var token = await _secureStorage.GetAsync("access_token");
if (token is not null)
{
request.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
return await base.SendAsync(request, cancellationToken);
}
}
DelegatingHandler-lähestymistapa pitää autentikaatiologiikan yhdessä paikassa eikä sitä tarvitse toistaa joka API-kutsussa erikseen. Puhdasta ja ylläpidettävää.
Parhaat käytännöt
Tässä vielä tärkeimmät ohjeet, jotka kannattaa pitää mielessä Preferencesin ja SecureStoragen kanssa työskennellessä:
- Älä koskaan tallenna salasanoja, tokeneja tai API-avaimia Preferencesiin. Preferences ei tarjoa salausta — data on luettavissa selkotekstinä laitteella. Tämä on yllättävän yleinen virhe.
- Käytä vakioavaimia. Määritä avaimet
static class-luokkaan kirjoitusvirheiden välttämiseksi:public static class PreferenceKeys { public const string Theme = "theme"; public const string FontSize = "font_size"; public const string NotificationsEnabled = "notifications_enabled"; } public static class SecureKeys { public const string AccessToken = "access_token"; public const string RefreshToken = "refresh_token"; } - Käytä aina DI-rajapintoja. Injektoi
IPreferencesjaISecureStoragekonstruktoriin staattistenPreferences.Default/SecureStorage.Default-kutsujen sijaan. Tämä mahdollistaa yksikkötestaamisen mock-objekteilla. - Kääri SecureStorage-kutsut try-catch-lohkoon.
SetAsyncjaGetAsyncvoivat heittää poikkeuksia, erityisesti Android-laitteilla joissa Keystore on korruptoitunut. - Älä tallenna suuria tietomääriä. Molemmat API:t on suunniteltu pienille avain-arvo -pareille. Suuremmille datamäärille käytä SQLitea.
- Huomioi iCloud-synkronointi iOS:ssä. Keychain-data voi synkronoitua iCloudin kautta eri laitteille. Sovelluksen poistaminen ei välttämättä poista SecureStorage-arvoja — tämä voi yllättää sekä kehittäjän että käyttäjän.
Yhteenveto
Preferences ja SecureStorage ovat .NET MAUI:n kevyimmät tallennusratkaisut, ja yhdessä SQLiten kanssa ne kattavat käytännössä kaikki paikallisen tiedonhallinnan tarpeet. Preferences hoitaa käyttäjäasetukset ja sovellustilat, SecureStorage huolehtii arkaluonteisesta datasta alustakohtaisella salauksella.
Kun yhdistät nämä MVVM-arkkitehtuuriin ja riippuvuusinjektioon — kuten tässä oppaassa teimme — saat selkeän, testattavan ja turvallisen tavan hallita sovelluksen dataa kaikilla alustoilla. Eikä siihen tarvita mitään monimutkaista.
Usein kysytyt kysymykset
Onko SecureStorage täysin turvallinen arkaluonteiselle datalle?
SecureStorage käyttää alustakohtaisia salausmekanismeja (Android EncryptedSharedPreferences, iOS Keychain, Windows DataProtectionProvider), jotka ovat kunkin alustan suositeltuja käytäntöjä. Se on huomattavasti turvallisempi kuin Preferences, mutta ei ole tarkoitettu äärimmäisen korkean turvaluokituksen datalle. Jos käsittelet todella kriittistä tietoa, kannattaa harkita lisäsalausta sovellustasolla.
Mitä tapahtuu SecureStoragen datalle kun sovellus poistetaan?
Androidissa data poistuu yleensä sovelluksen mukana, paitsi jos Auto Backup on käytössä. iOS:ssä tilanne on monimutkaisempi — Keychain-data saattaa säilyä laitteella tai synkronoitua iCloudin kautta, jolloin se voi olla saatavilla vielä sovelluksen uudelleenasennuksen jälkeen. Windowsissa pakattujen sovellusten data poistuu sovelluksen mukana.
Voiko Preferencesin ja SecureStoragen dataa jakaa sovellusten välillä?
Preferences tukee sharedName-parametria, joka mahdollistaa tiedon jakamisen esimerkiksi pääsovelluksen ja laajennusten välillä. SecureStorage ei suoraan tue sovellusten välistä jakoa, koska salaus on sidottu sovelluksen identiteettiin.
Miksi SecureStorage on asynkroninen mutta Preferences synkroninen?
SecureStorage käyttää alustan salauspalveluja, jotka voivat sisältää levyoperaatioita ja salausavainten käsittelyä — nämä toiminnot saattavat kestää hetken eivätkä saa blokata käyttöliittymäsäiettä. Preferences sen sijaan käyttää kevyempiä natiivirajapintoja, jotka ovat käytännössä aina välittömiä.
Kuinka paljon dataa Preferencesiin ja SecureStorageen voi tallentaa?
Molemmat on suunniteltu pienille tietomäärille. Windows-alustalla on konkreettiset rajoitukset: avaimen nimen enimmäispituus 255 merkkiä, yksittäisen arvon koko enintään 8 kilotavua. Android ja iOS eivät aseta tarkkoja kokorajoja, mutta suorituskyky heikkenee suurten tietomäärien kanssa. Jos huomaat tarvitsevasi enemmän tallennustilaa, SQLite on oikea valinta.