Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/dart.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Flutter CI

on:
push:
branches: [ "develop" ]
pull_request:
branches: [ "develop" ]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3.35.4
cache: true

- name: Verify Flutter version
run: flutter --version

- name: Install dependencies
run: flutter pub get

- name: Generate files
run: dart run build_runner build --delete-conflicting-outputs

- name: Analyze project
run: flutter analyze
100 changes: 98 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,99 @@
# flutter_base_project
# Flutter Base Project (Movie App Demo)

A new Flutter project.
A modern, responsive Flutter application showcasing a Clean Architecture implementation with a robust set of libraries and tools. This project serves as a comprehensive base for building scalable Flutter apps, currently demonstrating a Movie Database integration.

## 🚀 Tech Stack

- **State Management**: [flutter_bloc](https://pub.dev/packages/flutter_bloc)
- **Routing**: [go_router](https://pub.dev/packages/go_router)
- **Networking**: [dio](https://pub.dev/packages/dio) & [retrofit](https://pub.dev/packages/retrofit)
- **JSON Parsing**: [json_serializable](https://pub.dev/packages/json_serializable) & [json_annotation](https://pub.dev/packages/json_annotation)
- **Data Comparison**: [equatable](https://pub.dev/packages/equatable)
- **Image Caching**: [cached_network_image](https://pub.dev/packages/cached_network_image)
- **Local Storage**: [shared_preferences](https://pub.dev/packages/shared_preferences)

---

## 🏗 Project Structure

The project strictly follows **Clean Architecture** principles, dividing the code into discrete, decoupled layers:

```text
lib/
├── core/
│ ├── common/ # Shared elements like AppTheme and ThemeCubit (Light/Dark Mode)
│ ├── configs/ # App-wide configurations and constants
│ ├── database/ # Local storage solutions (e.g., SharedPreferences)
│ ├── failure/ # Error handling and failure models
│ ├── network/ # Dio client, API Constants, Retrofit ApiClients
│ └── utils/ # Helper functions and utilities
├── data/
│ ├── models/ # Data models (Responses, Entities, Enums) bridging JSON and UI
│ └── repositories/ # Concrete implementations for data fetching and caching
├── router/ # GoRouter configuration and route definitions
├── ui/ # User Interface layer (pages and shared widgets)
│ ├── pages/ # Organized by Feature (e.g., home, detail, onboarding)
│ │ ├── detail/ # Movie detail screen, cubit, and navigator
│ │ ├── home/ # Home listing screen, cubit, navigator, and local widgets
│ │ └── onboarding/ # First-time user experience and onboarding bloc
│ │
│ └── widgets/ # Reusable, completely stateless UI components
│ ├── appbars/ # Custom AppBars
│ ├── buttons/ # Reusable buttons
│ ├── images/ # Network image wrappers
│ └── scaffold/ # Base scaffold widgets
└── main.dart # Application entry point & root BlocProviders
```

---

## 🛠 Setup & Installation

**1. Clone the repository and install dependencies**
```bash
git clone <repository-url>
cd flutter_base_project
flutter pub get
```

**2. Run Code Generation (Crucial for Retrofit / JSON Serializable)**
Because this project utilizes code generation for API Clients and JSON parsing, you must run `build_runner` before the project can compile successfully.

```bash
# Run one time to build generated files (.g.dart)
dart run build_runner build --delete-conflicting-outputs

# OR run securely in the background checking for file changes
dart run build_runner watch --delete-conflicting-outputs
```

---

## ▶️ Running the App

You can run the app using the standard Flutter CLI commands:

```bash
# Run on connected device or simulator
flutter run

# Run in debug mode explicitly
flutter run --debug

# Run unit tests (if any)
flutter test

# Run static analysis to catch syntax or linting errors
flutter analyze
```

---

## 🌙 Features & Highlights
* **Dynamic Theming**: An interactive toggle inside the Home App Bar seamlessly switches between Light and Dark mode using a `ThemeCubit`.
* **Code-Generated API Routes**: Utilizes Retrofit to cleanly declare API routes on an interface, abstracting away complex parsing and boilerplate code.
* **Component Extraction**: All generic UI logic (Scaffolds, custom App Bars, Back Buttons, Network Images) are perfectly modularized into `lib/core/widgets` for instantaneous reuse.
9 changes: 9 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>

<!-- Deep linking setup -->
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="moviedb" android:host="app" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
Expand Down
15 changes: 15 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,20 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>flutter_base_project</string>
<key>CFBundleURLSchemes</key>
<array>
<string>moviedb</string>
</array>
</dict>
</array>
</dict>
</plist>
36 changes: 36 additions & 0 deletions lib/core/common/app_navigator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class BaseNavigator {
final BuildContext context;

BaseNavigator({required this.context});

Future<T?> pushNamed<T>(String name, {Object? arguments}) async {
return context.pushNamed<T>(name, extra: arguments);
}

Future<T?> push<T>(String location, {Object? arguments}) async {
return context.push<T>(location, extra: arguments);
}

void replaceNamed(String routeName, {Object? arguments}) async {
context.replaceNamed(routeName, extra: arguments);
}

void goNamed(String routeName, {Object? arguments}) {
context.goNamed(routeName, extra: arguments);
}

void go(String location, {Object? arguments}) {
context.go(location, extra: arguments);
}

void replace(String routeName, {Object? arguments}) {
context.replace(routeName, extra: arguments);
}

void pop() {
context.pop();
}
}
1 change: 1 addition & 0 deletions lib/core/common/theme/app_colors.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

File renamed without changes.
File renamed without changes.
10 changes: 10 additions & 0 deletions lib/core/configs/app_configs.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class AppConfigs {
static const String appName = 'Flutter Base Project';
static const String appVersion = '1.0.0';
static const String appBuildNumber = '1';

//Network
static const Duration connectTimeout = Duration(seconds: 10); //connectTimeout
static const Duration receiveTimeout = Duration(seconds: 10); //receiveTimeout
static const Duration sendTimeout = Duration(seconds: 10); //sendTimeout
}
89 changes: 89 additions & 0 deletions lib/core/failure/app_failure.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import 'dart:io';

import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart';

// Base Failure class
abstract class Failure extends Equatable {
final String message;

// You could add more properties like statusCode, stackTrace etc.

const Failure(this.message);

@override
List<Object?> get props => [message];

@override
String toString() => message; // Simple representation
}

// Specific Failure types (examples)
class ServerFailure extends Failure {
const ServerFailure({String message = 'An API error occurred'})
: super(message);
}

class NetworkFailure extends Failure {
const NetworkFailure({String message = 'Could not connect to the network'})
: super(message);
}

class CacheFailure extends Failure {
const CacheFailure({String message = 'Could not access local cache'})
: super(message);
}

class NotFoundFailure extends Failure {
const NotFoundFailure({String message = 'The requested item was not found'})
: super(message);
}

class UnexpectedFailure extends Failure {
const UnexpectedFailure({String message = 'An unexpected error occurred'})
: super(message);
}

class ApiFailure extends Failure {
const ApiFailure({String message = 'An unexpected error occurred'})
: super(message);
}

// Helper function to map exceptions to Failures (optional but useful)
Failure mapExceptionToFailure(dynamic e) {
// Add more specific exception handling (e.g., for DioError, SocketException)
if (e is FormatException) {
return ServerFailure(message: "Bad response format from server");
}

if (e is SocketException) {
return ServerFailure(message: "Network Error");
}

if (e is DioException) {
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout ||
e.type == DioExceptionType.connectionError ||
e.type == DioExceptionType.sendTimeout) {
return NetworkFailure(message: "Network Error");
}
if (e.response?.data is Map<String, dynamic>) {
dynamic message = e.response?.data['message'];
if (message is List && message.isNotEmpty && message.first is String) {
return mapMessageKeyFailure(message.first);
} else if (message is String) {
return mapMessageKeyFailure(message);
}
}
}

return UnexpectedFailure(message: "Error: $e");
}

Failure mapMessageKeyFailure(String? messageKey) {
if (messageKey == null) return const UnexpectedFailure();
switch (messageKey) {
default:
return UnexpectedFailure(message: messageKey);
}
}
4 changes: 2 additions & 2 deletions lib/core/network/api_clients.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';

import '../../data/models/movie_model.dart';
import '../../data/models/movie_response_model.dart';
import 'package:flutter_base_project/data/models/response/movie_model.dart';
import 'package:flutter_base_project/data/models/response/movie_response_model.dart';

part 'api_clients.g.dart';

Expand Down
6 changes: 4 additions & 2 deletions lib/core/network/dio_client.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_base_project/core/configs/app_configs.dart';
import 'api_constants.dart';

class DioClient {
Expand All @@ -9,8 +10,9 @@ class DioClient {
_dio = Dio(
BaseOptions(
baseUrl: ApiConstants.baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
connectTimeout: AppConfigs.connectTimeout,
sendTimeout: AppConfigs.sendTimeout,
receiveTimeout: AppConfigs.receiveTimeout,
queryParameters: {'api_key': ApiConstants.apiToken},
),
);
Expand Down
Loading