Table of Contents

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/​Lang​Wars

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

  1. Data fetching: The app calls the Stack Exchange API to retrieve the most-used tags, or loads a bundled offline JSON file (OfflineData.txt).
  2. Report generation: A FlexCelReport loads the template file (report.template.xlsx) from Android assets, binds the data table, and runs the report to produce a filled workbook with charts.
  3. HTML export: The generated workbook is exported to HTML using FlexCelHtmlExport with embedded SVG images for crisp vector charts.
  4. Display: The HTML is loaded into an Android WebView using base64 encoding.
  5. Sharing: A Share button lets you send the generated .xlsx file to other apps (email, cloud storage, etc.) via an Android Intent with FileProvider.

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 MemoryStream because 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_SEND with a FileProvider URI 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, ".");
        }

    }
}