Users expect a 'Download PDF' button, but developers face device memory limits, asynchronous generation, and complex multi-page layout. Recently, a client came to us with a report of 5,000 rows — on Android it triggered OutOfMemoryError, and we urgently re-architected to server-side generation. Our team has 5+ years of experience in implementing report generation systems for mobile apps — we guarantee stable operation even with large data volumes. We will evaluate your project and offer a cost-optimal solution.
Without the right approach, reports can consume too much memory, throw errors on opening, or display empty rectangles instead of Cyrillic. We've stepped on these rakes on dozens of projects and know how to avoid typical pitfalls. As Apple recommends: Use UIActivityViewController to let users export files.
How to implement report generation in a mobile app
Successful implementation requires choosing an approach, formatting data, rendering, and exporting to the required format. Let's look at three main ways to generate PDF.
Three approaches to PDF generation
Native rendering via Canvas
On iOS — UIGraphicsPDFRenderer, the most controllable option. Draw each element manually via Core Graphics:
func generateReport(data: ReportData) -> Data {
let pageRect = CGRect(x: 0, y: 0, width: 595, height: 842) // A4 in points
let renderer = UIGraphicsPDFRenderer(bounds: pageRect)
return renderer.pdfData { context in
context.beginPage()
let ctx = context.cgContext
// Header
let titleFont = UIFont.boldSystemFont(ofSize: 18)
let title = data.title as NSString
title.draw(at: CGPoint(x: 40, y: 40), withAttributes: [
.font: titleFont,
.foregroundColor: UIColor.black
])
// Data table
drawTable(ctx, rows: data.rows, startY: 80, pageWidth: pageRect.width)
// If data doesn't fit — new page
if needsNewPage {
context.beginPage()
// continue...
}
}
}
Labor-intensive, but the result is precise control over pixel output. Good for a fixed report template.
On Android via PdfDocument:
val document = PdfDocument()
val pageInfo = PdfDocument.PageInfo.Builder(595, 842, 1).create()
val page = document.startPage(pageInfo)
val canvas = page.canvas
val paint = Paint().apply { textSize = 18f; isFakeBoldText = true }
canvas.drawText(data.title, 40f, 60f, paint)
document.finishPage(page)
val stream = ByteArrayOutputStream()
document.writeTo(stream)
document.close()
HTML → PDF via WebView
Generate an HTML template and convert it to PDF. Easier for complex layouts, tables, and text blocks — HTML/CSS is more flexible than Canvas. On Flutter use printing + pdf package:
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
Future<Uint8List> buildPdf(ReportData data) async {
final doc = pw.Document();
final font = await PdfGoogleFonts.notoSansRegular();
final boldFont = await PdfGoogleFonts.notoSansBold();
doc.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
build: (context) => [
pw.Header(text: data.title, textStyle: pw.TextStyle(font: boldFont, fontSize: 18)),
pw.SizedBox(height: 16),
pw.TableHelper.fromTextArray(
headers: data.headers,
data: data.rows,
cellStyle: pw.TextStyle(font: font, fontSize: 10),
),
],
),
);
return doc.save();
}
pw.MultiPage automatically splits into pages — solving the main problem of long reports.
Server-side generation (recommended for complex reports)
Reports with large data volumes, complex charts, or corporate branding are better generated on the server. Puppeteer (Node.js) renders HTML + Charts into PDF with browser engine accuracy. The client receives a link or file via WebSocket after completion.
This is the only right path if the report needs interactive charts (Echarts, Highcharts) — you can't render them on the device without WebView.
| Parameter | Native Canvas | HTML → PDF WebView | Server-side generation |
|---|---|---|---|
| Implementation complexity | High | Medium | Low (client) |
| Layout control | Full | Depends on CSS | Depends on HTML/CSS |
| Data volume | Up to 50 pages | Up to 100 pages | Unlimited |
| Interactive charts | No | Via WebView | Yes (Puppeteer) |
| Device memory | High | Medium | Low |
To choose the optimal solution, contact us — we will analyze your data and propose an approach.
Which report format to choose?
Besides PDF, CSV and Excel are often required. CSV is the lightest: data is written in delimiter-separated rows. The main nuance is encoding: Excel opens CSV in UTF-8 only with BOM (\uFEFF). Otherwise, Cyrillic turns into gibberish. Excel (xlsx) is more complex: you need a library that supports formulas, styles, and multiple sheets. For simple tables, CSV is sufficient. In practice, CSV with 10,000 rows generates on the device in under a second, while PDF with the same number of rows requires pagination.
| Format | Implementation complexity | Style support | File size | Universality |
|---|---|---|---|---|
| High | Yes (fixed layout) | Medium | High (opens everywhere) | |
| CSV | Low | No | Small | Medium (Excel, Numbers) |
| Excel | Medium | Yes (formulas, colors) | Large | High (Excel, Google Sheets) |
What to choose: native rendering or HTML in PDF?
The choice depends on the complexity of the layout and data volume. For a fixed template with a small number of pages, native rendering is suitable. For complex tables and charts — HTML to PDF or server. Our experience shows that 80% of projects use a hybrid approach: simple reports on the device, complex ones on the server. We will help you choose the optimal turnkey solution.
CSV export
Simpler than PDF, but there are encoding nuances. Excel expects UTF-8 with BOM — otherwise Cyrillic displays as garbage. On Flutter:
String buildCsv(List<List<dynamic>> rows) {
final buffer = StringBuffer();
buffer.write('\uFEFF'); // UTF-8 BOM for correct opening in Excel
for (final row in rows) {
buffer.writeln(row.map((cell) {
final str = cell.toString();
// Escape cells with commas and quotes
return str.contains(',') || str.contains('"')
? '"${str.replaceAll('"', '""')}"'
: str;
}).join(','));
}
return buffer.toString();
}
Sharing the generated file
share_plus on Flutter, UIActivityViewController on iOS, FileProvider + Intent.ACTION_SEND on Android. Saving to gallery / Files — via path_provider + open_filex.
On iOS you need NSPhotoLibraryAddUsageDescription in Info.plist for saving to Photos, UIFileSharingEnabled for file access via Files.app.
What typical problems occur during report generation?
Generating a PDF with a table of 10,000 rows on the device — OutOfMemoryError on Android or memory warning on iOS. Solution: data pagination, page-by-page generation, for large volumes — server.
Cyrillic in PDF without an embedded font — white rectangles instead of letters. Be sure to embed a custom font with Unicode support via pw.Font.ttf(...).
Problems with Excel: if using a library like excel (Dart), don't forget about row styles — otherwise the file won't open in old versions of Excel. We recommend always testing on real office suite versions.
Steps to implement report generation
- Analyze data and layouts: determine the format, volume, and type of reports.
- Choose an approach: native, HTML→PDF, or server-side.
- Develop templates with layout and font embedding.
- Integrate export and sharing.
- Test with real data (empty, large, with Cyrillic).
- Optimize memory and performance.
What's included in the work
- Choosing the approach based on data specifics and layout (native / HTML→PDF / server)
- Implementing report templates (PDF/CSV/Excel)
- Integrating file sharing and saving
- Handling edge cases: empty data, large volumes, page breaks
Timeframes
One PDF report template with basic formatting: 2–3 days. Multiple report types with charts and server-side generation: 1–2 weeks. Cost is calculated individually. Get a consultation for your project — we will implement a turnkey reporting system. Order development, and we will offer the optimal solution considering your data and budget.
More about Excel libraries
For Flutter, use the excel package (version 4.x) — it supports styles, cell merging, and formulas. For Android — Apache POI (via JNI or server). For iOS — SwiftExcel or xlsxwriter. The choice depends on report complexity and development time.







