LangWars (Android Native) (C# / Mobile / Android-Native)
Note
This demo is available in your FlexCel installation at <FlexCel Install Folder>\samples\csharp\VS2026\Mobile\Android-Native\LangWars and also at https://github.com/tmssoftware/TMS-FlexCel.NET-demos/tree/master/csharp/VS2026/Mobile/Android-Native/LangWars
Overview
This example shows how to generate a report with FlexCel on Android. It fetches the most popular programming language tags from the Stack Overflow API, fills a pre-designed Excel template with the data, and exports the result to HTML for display in a WebView. You can switch between online and offline data in case you don't have internet access.
How It Works
- Data fetching: The app calls the Stack Exchange API to retrieve the most-used tags, or loads a bundled offline JSON file (
OfflineData.txt). - Report generation: A
FlexCelReportloads the template file (report.template.xlsx) from Android assets, binds the data table, and runs the report to produce a filled workbook with charts. - HTML export: The generated workbook is exported to HTML using
FlexCelHtmlExportwith embedded SVG images for crisp vector charts. - Display: The HTML is loaded into an Android
WebViewusing base64 encoding. - Sharing: A Share button lets you send the generated
.xlsxfile to other apps (email, cloud storage, etc.) via an Android Intent withFileProvider.
FlexCel Features Demonstrated
- FlexCelReport: Template-based report generation with
AddTable()for data binding - FlexCelHtmlExport: Excel-to-HTML conversion with SVG images, HTML5 support, and embedded images
- XlsFile: Loading and saving Excel workbooks
Implementation Details
- Renderer.cs: Core logic for fetching data, running the report, and generating HTML. Asset streams are copied to
MemoryStreambecause Android asset streams are non-seekable. - LangData.cs: Data models with AOT-friendly source-generated JSON serialization (
JsonSerializerContext). - FileSender.cs: Shares the generated Excel file using
Intent.ACTION_SENDwith aFileProviderURI and read permissions. - MainActivity.cs: Hosts the WebView, Fight button, Online/Offline toggle, and Share button.
Files
FileSender.cs
using Android.Content;
using FlexCel.Core;
namespace LangWars
{
internal static class FileSender
{
public static async Task SendFile(Activity activity, string filePath, bool offline)
{
// To send the file, we need to define a file provider in AndroidManifest.xml
// See https://doc.tmssoftware.com/flexcel/net/guides/android-guide.html#sharing-files
if (!File.Exists(filePath))
{
await Renderer.Display(offline);
}
Intent Sender = new(Intent.ActionSend);
Sender.SetType(StandardMimeType.Xlsx);
Java.IO.File xlsFile = new(filePath);
var contentUri = AndroidX.Core.Content.FileProvider.GetUriForFile(activity, activity.ApplicationContext!.PackageName + ".fileprovider", xlsFile);
Sender.PutExtra(Intent.ExtraStream, contentUri);
Sender.SetFlags(ActivityFlags.GrantReadUriPermission);
activity.StartActivity(Intent.CreateChooser(Sender, "Select application"));
}
}
}
LangData.cs
using System;
using System.Drawing;
using System.Runtime.Serialization;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace LangWars;
public sealed record class LangDataList(
[property: JsonPropertyName("items")] IReadOnlyList<LangData> Items);
public sealed record class LangData(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("count")] int Count);
[JsonSerializable(typeof(LangDataList))]
internal partial class LangDataListContext : JsonSerializerContext
{
}
MainActivity.cs
using Android.Content;
using Android.Content.PM;
using Android.Content.Res;
using Android.Views;
using Android.Webkit;
namespace LangWars
{
[Activity(Label = "@string/app_name", MainLauncher = true, Theme = "@android:style/Theme.Material.Light.NoActionBar",
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)]
public class MainActivity : Activity
{
private WebView webView = null!;
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.activity_main);
var toolbar = FindViewById<Toolbar>(Resource.Id.toolbar);
SetActionBar(toolbar);
webView = FindViewById<WebView>(Resource.Id.webview)!;
webView.SetWebViewClient(new WebViewClient());
var btnFight = FindViewById<Button>(Resource.Id.btn_fight)!;
btnFight.Click += async (sender, e) => await OnFight();
var switchLabel = FindViewById<TextView>(Resource.Id.switch_label)!;
var switchOnline = FindViewById<Switch>(Resource.Id.switch_online)!;
switchOnline.CheckedChange += (sender, e) =>
{
switchLabel.Text = e.IsChecked ? "Online" : "Offline";
};
var btnShare = FindViewById<ImageButton>(Resource.Id.btn_share)!;
btnShare.Click += async (sender, e) =>
{
await FileSender.SendFile(this, Renderer.TempXlsPath, IsOffline);
};
}
private bool IsOffline => !FindViewById<Switch>(Resource.Id.switch_online)!.Checked;
private async Task OnFight()
{
var spinner = FindViewById<ProgressBar>(Resource.Id.spinner)!;
var btnFight = FindViewById<Button>(Resource.Id.btn_fight)!;
spinner.Visibility = ViewStates.Visible;
btnFight.Enabled = false;
try
{
try
{
string filePath = await Renderer.Display(IsOffline);
string html = File.ReadAllText(filePath);
SetHTML(html);
}
finally
{
spinner.Visibility = ViewStates.Gone;
btnFight.Enabled = true;
}
}
catch (Exception ex)
{
SetHTML("<html>" + System.Security.SecurityElement.Escape(ex.Message) + "</html>");
}
}
public override void OnConfigurationChanged(Configuration newConfig)
{
base.OnConfigurationChanged(newConfig);
ReloadLastHtml();
}
private void ReloadLastHtml()
{
string path = Renderer.TempHtmlPath;
if (File.Exists(path))
{
string html = File.ReadAllText(path);
SetHTML(html);
}
}
void SetHTML(string html)
{
string centered = html.Replace("<body", "<body style=\"display:flex;justify-content:center;\"");
string base64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(centered));
webView.LoadData(base64, "text/html; charset=UTF-8", "base64");
}
}
}
Renderer.cs
using FlexCel.Core;
using FlexCel.Render;
using FlexCel.Report;
using FlexCel.XlsAdapter;
using System.Text.Json;
namespace LangWars
{
public static class Renderer
{
public static string TempXlsPath
{
get
{
return Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal), "langwars.xlsx");
}
}
public static string TempHtmlPath
{
get
{
return Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal), "langwars.html");
}
}
public static async Task<string> Display(bool offline)
{
var Langs = offline ? await LoadData() : await FetchData();
Langs = Langs with { Items = Langs.Items.OrderByDescending(l => l.Count).ToList() };
var xls = RunReport(Langs);
var dir = Path.GetDirectoryName(TempXlsPath)!;
Directory.CreateDirectory(dir);
xls.Save(TempXlsPath); //we save it to share it and to display it on startup.
GenerateHTML(xls);
return TempHtmlPath;
}
static async Task<LangDataList> FetchData()
{
using var httpClient = new HttpClient();
var responseMessage = await httpClient.GetAsync("https://api.stackexchange.com/2.3/tags?order=desc&sort=popular&site=stackoverflow&pagesize=5");
responseMessage.EnsureSuccessStatusCode();
using var stream = await responseMessage.Content.ReadAsStreamAsync();
var result = await JsonSerializer.DeserializeAsync(stream, LangDataListContext.Default.LangDataList);
return result ?? throw new InvalidOperationException("Failed to deserialize language data from API response.");
}
static async Task<LangDataList> LoadData()
{
using var offlineData = Android.App.Application.Context.Assets!.Open("OfflineData.txt");
{
var result = await JsonSerializer.DeserializeAsync(offlineData, LangDataListContext.Default.LangDataList);
return result ?? throw new InvalidOperationException("Failed to deserialize offline language data.");
}
}
private static ExcelFile RunReport(LangDataList langs)
{
ExcelFile Result = new XlsFile(true);
using (var template = Android.App.Application.Context.Assets!.Open("report.template.xlsx"))
{
//we can't load directly from the asset stream, as we need a seekable stream.
using var memtemplate = new MemoryStream();
template.CopyTo(memtemplate);
memtemplate.Position = 0;
Result.Open(memtemplate);
}
using (FlexCelReport fr = new(true))
{
fr.AddTable("lang", langs.Items);
fr.Run(Result);
}
return Result;
}
static void GenerateHTML(ExcelFile xls)
{
using FlexCelHtmlExport html = new(xls, true);
//If we were using png, we would have to set
//a high resolution so this looks nice in high resolution displays.
//html.ImageResolution = 326;
//but we will use SVG, which is vectorial:
html.HtmlVersion = THtmlVersion.Html_5;
html.SavedImagesFormat = THtmlImageFormat.Svg;
html.EmbedImages = true;
html.Export(TempHtmlPath, ".");
}
}
}