Flutter for Beginners: Getting Started with Mobile App Development
When I decided to build my first mobile app, I had a decision to make: learn Swift and Kotlin separately, or find a cross-platform solution that would let me ship on both iOS and Android from a single codebase. I chose Flutter, and after building several production apps with it — including DocLite, a PDF viewer, and Bloomcard, a flashcard app — I can say it was the right call.
Flutter is Google's open-source UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase. It uses the Dart programming language and renders everything using its own high-performance rendering engine, which means your UI looks and behaves consistently across platforms.
This guide will walk you through getting started with Flutter, from installation to understanding the core concepts you need to build real apps.
Why Flutter?
Before diving into the how, let me address the why. There are several cross-platform options available — React Native, Kotlin Multiplatform, .NET MAUI — so why choose Flutter?
Single codebase, truly. With Flutter, your UI code, business logic, and even many platform-specific features work across iOS and Android without modification. In my experience, I share about 95% of code between platforms.
Hot reload. This is Flutter's killer feature for developer experience. You make a change, save the file, and the change appears on your device or emulator in under a second. Without losing state. This means you can tweak a padding value and see the result instantly, which makes UI development feel like working in a design tool.
Widget system. Everything in Flutter is a widget, and widgets compose together in a way that is both flexible and intuitive once you internalize the pattern. The learning curve is real, but the productivity payoff is significant.
Growing ecosystem. The pub.dev package ecosystem is mature and growing. For most common needs — HTTP requests, state management, database, navigation — there are well-maintained packages available.
Performance. Flutter compiles to native ARM code. It does not use a JavaScript bridge like React Native. In practice, this means smooth 60fps animations and fast startup times.
Setting Up Your Environment
Flutter installation is straightforward, though it requires a few dependencies.
Step 1: Download the Flutter SDK. Visit the official Flutter website and download the SDK for your operating system. Extract it to a location on your system and add the flutter/bin directory to your PATH.
Step 2: Run Flutter Doctor. Open a terminal and run:
flutter doctor
This command checks your environment and tells you what is missing. It will flag issues like missing Android SDK components, Xcode not being installed (macOS), or missing VS Code extensions.
Step 3: Set up an editor. VS Code with the Flutter extension is my recommendation for beginners. It provides code completion, widget preview, debugging tools, and an integrated device selector. Android Studio also works well and comes with Android SDK management built in.
Step 4: Set up a device. You can use a physical device or an emulator. For Android, create an AVD (Android Virtual Device) through Android Studio's Device Manager. For iOS (macOS only), the iOS Simulator comes with Xcode.
Step 5: Verify everything works.
flutter create my_first_app
cd my_first_app
flutter run
If you see the Flutter demo app on your device or emulator, you are ready to start building.
Dart Basics
Flutter uses Dart, a language developed by Google. If you know JavaScript, Java, or Kotlin, Dart will feel familiar. Here are the essentials:
Variables and types:
// Type inference
var name = 'Bitnara';
final age = 25; // Cannot be reassigned
const pi = 3.14159; // Compile-time constant
// Explicit types
String greeting = 'Hello';
int count = 42;
double price = 9.99;
bool isActive = true;
List<String> tags = ['flutter', 'dart', 'mobile'];
Map<String, int> scores = {'math': 95, 'science': 88};
Null safety: Dart has sound null safety, which means variables cannot be null unless you explicitly say so:
String name = 'Bitnara'; // Cannot be null
String? nickname; // Can be null
// Null-aware operators
print(nickname?.length); // null (no crash)
print(nickname ?? 'No nickname'); // 'No nickname'
Functions:
// Regular function
int add(int a, int b) {
return a + b;
}
// Arrow syntax for single expressions
int multiply(int a, int b) => a * b;
// Named parameters (very common in Flutter)
void greet({required String name, String title = 'Mr.'}) {
print('Hello, $title $name');
}
greet(name: 'Kim'); // Hello, Mr. Kim
Classes:
class Task {
final String title;
final bool isCompleted;
const Task({required this.title, this.isCompleted = false});
Task copyWith({String? title, bool? isCompleted}) {
return Task(
title: title ?? this.title,
isCompleted: isCompleted ?? this.isCompleted,
);
}
}
Async/Await: Dart handles asynchronous operations with Futures and the async/await syntax:
Future<String> fetchData() async {
final response = await http.get(Uri.parse('https://api.example.com/data'));
return response.body;
}
These basics will carry you through most of Flutter development. The language is designed to be approachable, and its type system catches many errors at compile time rather than runtime.
The Widget Tree
Everything in Flutter is a widget. The screen is a widget. The layout is a widget. The text is a widget. The padding around the text is a widget. Even the app itself is a widget.
Widgets are composed into a tree structure. Here is a minimal Flutter app:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('My First App')),
body: const Center(
child: Text('Hello, Flutter!'),
),
),
);
}
}
The hierarchy here is: MyApp → MaterialApp → Scaffold → AppBar + Center → Text. Each widget describes a piece of the UI, and Flutter builds the screen by composing them together.
This composition model is powerful. Need padding? Wrap with Padding. Need to make something tappable? Wrap with GestureDetector. Need to constrain the size? Wrap with SizedBox or ConstrainedBox. You build complex UIs by nesting simple widgets.
StatelessWidget vs. StatefulWidget
Flutter has two fundamental types of widgets:
StatelessWidget: Does not change after it is built. Its output depends only on its constructor parameters. Use this for static content.
class WelcomeHeader extends StatelessWidget {
final String username;
const WelcomeHeader({super.key, required this.username});
@override
Widget build(BuildContext context) {
return Text('Welcome, $username!',
style: Theme.of(context).textTheme.headlineMedium,
);
}
}
StatefulWidget: Can change over time. It maintains mutable state that triggers a rebuild when updated.
class CounterWidget extends StatefulWidget {
const CounterWidget({super.key});
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Count: $_count', style: const TextStyle(fontSize: 48)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _increment,
child: const Text('Increment'),
),
],
);
}
}
The key rule: call setState() whenever you change state that should trigger a UI update. Flutter will call build() again and update only the parts of the screen that actually changed.
Layout Widgets
Flutter's layout system uses widgets for everything. Here are the ones you will use constantly:
Column and Row: Arrange children vertically or horizontally.
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Title'),
const SizedBox(height: 8),
const Text('Subtitle'),
const SizedBox(height: 16),
ElevatedButton(onPressed: () {}, child: const Text('Action')),
],
)
Container: A convenience widget that combines padding, margins, decoration, and constraints.
Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(color: Colors.black12, blurRadius: 8, offset: const Offset(0, 2)),
],
),
child: const Text('Card content'),
)
ListView: A scrollable list. For long lists, use ListView.builder which only builds visible items:
ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(tasks[index].title),
trailing: Checkbox(
value: tasks[index].isCompleted,
onChanged: (value) => toggleTask(index),
),
);
},
)
Expanded and Flexible: Control how children share available space within a Row or Column.
Row(
children: [
Expanded(flex: 2, child: TextField()),
const SizedBox(width: 8),
Expanded(flex: 1, child: ElevatedButton(onPressed: () {}, child: const Text('Send'))),
],
)
Navigation
For simple apps, Flutter's built-in Navigator works fine. But for production apps, I strongly recommend using a routing package. GoRouter is the one I use in all my projects:
final router = GoRouter(
routes: [
GoRoute(path: '/', builder: (context, state) => const HomeScreen()),
GoRoute(path: '/detail/:id', builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailScreen(id: id);
}),
GoRoute(path: '/settings', builder: (context, state) => const SettingsScreen()),
],
);
// Navigate
context.go('/detail/42');
context.push('/settings');
context.pop();
GoRouter gives you declarative routing, deep linking support, and path parameters out of the box. It is maintained by the Flutter team and is the recommended routing solution.
State Management
State management is the most debated topic in Flutter. There are many options: Provider, Riverpod, Bloc, GetX, MobX, and more. For beginners, I recommend starting with Riverpod. It has a steeper initial learning curve than Provider, but it scales much better and avoids the pitfalls that trip up Provider users in larger apps.
Here is a simple Riverpod example:
// Define a provider
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
void decrement() => state--;
}
// Use it in a widget
class CounterScreen extends ConsumerWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
The beauty of Riverpod is that providers are global but testable, and the ref.watch mechanism automatically rebuilds widgets when the state they depend on changes.
Your First Real App
With these fundamentals, you can build something real. Here is my suggestion for a first project: a simple task list app. It covers:
- Displaying a list of items (ListView)
- Adding items (TextFormField + button)
- Toggling items (Checkbox)
- Persisting data (shared_preferences or a local database)
- Navigation between screens
Start simple. Get the list displaying. Add the ability to create tasks. Add persistence. Each step teaches a core Flutter concept, and at the end you have a functional app you can actually use.
Building and Running
When you are ready to test on a real device or share your app:
# Debug build (fast, includes debugging tools)
flutter run
# Release build for Android
flutter build apk --release
# Release build for iOS (macOS only)
flutter build ios --release
The debug build includes hot reload and the Dart DevTools. The release build is optimized and stripped of debugging overhead. Always test your release build before publishing — performance characteristics can differ.
Flutter has become my go-to framework for mobile development. It is not perfect — the widget nesting can get deep, Dart is less popular than JavaScript or Kotlin, and some platform-specific features require plugins that may not be perfectly maintained. But for an indie developer who wants to ship on both platforms without maintaining two codebases, it is the best option available today. Start building, and the framework will teach you the rest.