enhost Blogs

Global Reader

Welcome to the Global Reader, your window into the vibrant community of enhost BLOGS! Here, you'll find a dynamic feed of the latest posts from all our users, showcasing the diverse voices and perspectives within our Fediverse network. Explore fresh ideas, discover new writers, and engage with content that sparks your curiosity. Dive in and see what the enhost BLOGS community is creating today!

from The Pragmatic Pixel

Hey everyone, Jamie here.

Greetings from a rather sunny London! I'm down for a weekend getaway, swapping the usual coding setup for some city exploration. Navigating a sprawling metropolis like this – the Tube, bustling streets, countless signs – always gets me thinking about how we design systems, both physical and digital, to be usable by everyone.

You see a lot of effort put into physical accessibility here: ramps, audio announcements on public transport, tactile paving. But you also notice the challenges – an old Tube station with endless stairs, a crowded pavement that's tricky to navigate with a pushchair or wheelchair. It struck me how similar the challenges and, more importantly, the goals are to what we aim for in digital accessibility (often shortened to a11y) when building our Laravel backends and Flutter apps.

From City Streets to Digital Interfaces

In a city, good accessibility means everyone, regardless of physical ability, can get around, access services, and participate in city life. In the digital world, it means ensuring our websites and applications can be easily used and understood by people with diverse abilities – including those with visual, auditory, motor, or cognitive impairments.

It’s not just a niche concern; it’s about fundamental usability.

Accessibility in Laravel (Web & APIs)

When we're building with Laravel, especially if it's serving web pages or a web-based admin panel, accessibility is key:

  1. Semantic HTML: This is the foundation. Using <nav>, <main>, <article>, <aside>, <button>, proper heading levels (<h1> to <h6>), and alt text for images provides inherent structure that screen readers and assistive technologies rely on. Laravel's Blade templating doesn't stop you from writing semantic HTML; it encourages it.
  2. ARIA Attributes: Accessible Rich Internet Applications (ARIA) attributes can enhance HTML by providing extra information to assistive technologies, especially for dynamic content or custom UI components. Use them judiciously where semantic HTML alone isn't enough.
  3. Keyboard Navigation: Ensure all interactive elements (links, buttons, form fields) are focusable and operable using only a keyboard. Test your tabindex flow.
  4. Form Handling: Clearly associate labels with form inputs (<label for="id">). Provide clear validation messages (Laravel's validation system is great for this, just ensure they're presented accessibly).
  5. API Design: While an API itself isn't “viewed,” the data it returns needs to be structured clearly so that any client consuming it (including a Flutter app designed with accessibility in mind) can easily parse and present it in an accessible way. Error messages from the API should also be clear and understandable.

Accessibility in Flutter (Mobile Apps)

Flutter has made significant strides in providing excellent built-in support for accessibility:

  1. Semantics Widget: This is your primary tool. Flutter widgets often create a semantics tree automatically, but you can use the Semantics widget to explicitly describe the meaning of your UI elements for assistive technologies. You can add labels, hints, indicate if something is a button, a header, etc.
  2. Screen Reader Support (TalkBack/VoiceOver): Much of this comes “for free” if you use standard Material or Cupertino widgets correctly and provide good semantic information. Always test with screen readers enabled!
  3. Sufficient Touch Target Sizes: Ensure buttons and interactive elements are at least 48x48 logical pixels, as recommended by Material Design and Apple's HIG, to be easily tappable.
  4. Color Contrast: Use tools to check that your text and UI elements have sufficient contrast against their background, making them readable for people with low vision or color blindness. The WCAG (Web Content Accessibility Guidelines) provide specific ratios.
  5. Font Scaling: Respect the user's device font size settings. Flutter generally handles this well, but test how your UI reflows with larger fonts.
  6. Haptic Feedback & Audio Cues: These can provide important non-visual feedback for interactions.

Why Does This Matter? The Pragmatic Angle

Beyond it simply being the “right thing to do,” there are very practical reasons to focus on accessibility:

  • Wider Audience: You're making your application usable by more people. The World Health Organization estimates that over a billion people live with some form of disability. That's a significant potential user base.
  • Legal Requirements: In many countries and sectors, there are legal obligations to ensure digital services are accessible (e.g., ADA in the US, EN 301 549 in Europe).
  • Better UX for Everyone (The Curb-Cut Effect): Features designed for accessibility often improve the user experience for all users. Think of curb cuts in pavements – designed for wheelchairs, but also used by people with strollers, luggage, or even just tired legs. Clearer UIs, larger tap targets, and captions benefit everyone.
  • SEO (for web): Semantic HTML and good structure, key for accessibility, also contribute positively to Search Engine Optimization.
  • Brand Reputation: Demonstrating a commitment to inclusivity can enhance your brand's image.

Small Steps, Big Impact

Reflecting on navigating London, it's clear that making a complex system truly accessible is an ongoing effort, full of big projects and small adjustments. The same is true for our apps.

You don't have to become an a11y expert overnight. Start by:

  • Learning the basics for your chosen platforms (Laravel and Flutter).
  • Testing with a keyboard and enabling screen readers during development.
  • Using accessibility checkers and linters.
  • Thinking about users with different needs from the design phase, not as an afterthought.

Even small, consistent efforts can make a huge difference in creating digital experiences that, much like a well-designed city, are welcoming and usable for everyone.

Enjoy the rest of your weekend – I'm off to see if I can find a truly step-free route to that coffee shop I spotted earlier!

Cheers,

Jamie C

 
Read more...

from The Pragmatic Pixel

Hey everyone, Jamie here.

So, you've poured your heart and soul into your Flutter app. The UI is slick, the features are robust, your Laravel backend is purring along, and you've tested it until you can't see straight. Now comes the final hurdle before your creation reaches the masses: the app store approval process.

Ah, yes. For many developers, this phase can feel like a black box, a nerve-wracking wait, and sometimes, a source of immense frustration. Both Google's Play Store and Apple's App Store have their own set of gates, guidelines, and review processes. While they share the common goal of ensuring quality, security, and a good user experience, their approaches and pain points can differ.

Let's take a pragmatic look at what to expect and how to navigate this crucial step.

The Common Ground: Quality Control

Before diving into specifics, it's important to remember that both platforms are trying to:

  • Protect users from malware, scams, and inappropriate content.
  • Ensure apps function as described and provide a baseline level of quality.
  • Maintain the integrity of their respective ecosystems.
  • Verify that apps adhere to their specific business and content policies.

So, while the details vary, a well-built, thoroughly tested app that clearly respects user privacy and platform guidelines has a much better chance from the outset.

Google Play Store (Android): The Broader Gates

Google's process is generally perceived as being faster and more automated, especially for initial submissions or updates.

  1. Google Play Console: This is your mission control. You'll need a developer account (a one-time fee).
  2. App Information (Store Listing): You'll provide your app's name, short and long descriptions, screenshots (phone, tablet, feature graphic), promo video, categorization, and contact details. Keywords are important here for discoverability.
  3. Content Rating: You'll complete a questionnaire to determine the age rating for your app. Be honest!
  4. Privacy Policy: Essential. You must link to a privacy policy, especially if your app collects any user data (which most do).
  5. Uploading Your App Bundle (AAB) or APK: AAB is now the standard and recommended format.
  6. Testing Tracks: Google offers excellent testing tracks:
    • Internal Testing: For quick distribution to a small, trusted team.
    • Closed Testing: For wider beta tests with specific groups (e.g., via email lists).
    • Open Testing: Allows users to opt-in to your beta program directly from the Play Store. Leverage these extensively!
  7. Review Times: Initial reviews might take a few days, but updates are often live within hours, sometimes even faster, thanks to a lot of automated checks. However, if an issue is flagged, it can go into a more detailed manual review.
  8. Common Rejection Reasons:
    • Metadata Issues: Misleading descriptions, incorrect categorization, low-quality screenshots.
    • Permissions Abuse: Requesting permissions your app doesn't clearly need.
    • Content Policy Violations: Inappropriate content, intellectual property infringement.
    • Security Vulnerabilities: Though less common for typical Flutter/Laravel apps unless you're doing something very low-level.
    • Broken Functionality: Obvious crashes or features that don't work as advertised.

Apple App Store (iOS): The Walled Garden

Apple's review process is notoriously more stringent and has historically involved more manual review, though they've also incorporated more automation.

  1. Apple Developer Program: Requires an annual subscription.
  2. App Store Connect: This is where you manage your app, its metadata, builds, and submissions.
  3. App Information: Similar to Google, but often with more scrutiny on the quality and accuracy of screenshots, descriptions, and keywords. Ensure your app name and subtitle are compelling and accurate.
  4. Privacy “Nutrition Labels”: You need to declare what data your app collects and how it's used, which is displayed publicly on your App Store page. Be thorough and transparent.
  5. TestFlight: Apple's platform for beta testing. You can invite internal testers (your team) and external testers (up to 10,000 users via email or public link). External tester builds still go through a (usually quicker) beta review.
  6. Uploading Your Build: Typically done via Xcode or the Transporter app.
  7. Review Times: This is the big one. While it has improved, expect reviews to take anywhere from 24 hours to several days, sometimes longer, especially for new apps or apps with significant changes. Updates also go through review.
  8. Common Rejection Reasons:
    • Guideline 4.3 – Spam/Repetitive Apps: If your app is too similar to others or deemed “low quality” or a “copycat.” This is a common and sometimes frustrating one.
    • Performance & Crashes: Apps that crash frequently or perform poorly will be rejected.
    • User Interface (UI) / User Experience (UX): Apple has strong Human Interface Guidelines (HIG). Apps that don't feel “native” or have confusing navigation can be rejected.
    • Incomplete Information / Broken Links: Ensure all links (support, privacy policy) work and all required demo information is provided.
    • Misleading Users: Claims in your description that the app doesn't fulfill.
    • Inappropriate Content or Use of APIs.
    • Payments: Complex rules around in-app purchases and subscriptions.

Key Differences to Keep in Mind

  • Strictness: Apple is generally stricter, particularly on UI/UX and perceived app “value.”
  • Review Speed: Google is usually faster for updates.
  • Flexibility: Android is a more open platform, leading to a wider variety of apps (and sometimes quality). Apple maintains tighter control.
  • Feedback: Both provide feedback on rejections, but sometimes it can be generic. You may need to correspond with the review team for clarification.

Tips for a Smoother Approval Journey

  1. READ THE GUIDELINES! This cannot be stressed enough.
    • Google Play Developer Policy Center.
    • Apple App Store Review Guidelines (and the Human Interface Guidelines).
  2. Test, Test, Test: On multiple devices, different OS versions. Fix crashes. Polish the UX.
  3. Accurate & Compelling Metadata: Your store listing is your shop window. Make it shine, but be honest.
  4. Solid Privacy Policy: Have one, make it accessible, and ensure it accurately reflects your data practices.
  5. Demo Account & Instructions: If your app requires login, always provide a demo account and clear instructions for the reviewer. This is a major cause of delays/rejections.
  6. Clear Review Notes: Use the “Notes for Reviewer” section to explain any non-obvious features, why you need certain permissions, or to point out specific areas you'd like them to focus on.
  7. Be Patient & Professional: If rejected, read the feedback carefully. Make the necessary changes. If you genuinely believe there's a misunderstanding, you can appeal, but do so politely and with clear reasoning.
  8. Iterate with Beta Testing: Use TestFlight and Google Play testing tracks to get real user feedback and catch issues before official review.

It's a Marathon, Not a Sprint

The app store approval process is a necessary part of bringing a mobile application to the world. It can be challenging, but by understanding the requirements, preparing thoroughly, and being diligent, you can significantly increase your chances of a smooth submission.

What are your biggest app store review war stories or top tips? Share them in the comments below!

Cheers,

Jamie C

 
Read more...

from The Pragmatic Pixel

Hey everyone, Jamie here.

We've all been there: You're on the train, underground, or just dealing with spotty Wi-Fi, and your app becomes a digital paperweight the moment connectivity drops. Meanwhile, users are trying to capture that important note, complete a task, or continue working – but everything grinds to a halt because your app can't reach the server.

This is where offline-first design and data synchronization become game-changers. Building apps that work seamlessly offline and intelligently sync when connectivity returns isn't just a nice-to-have anymore – it's what users expect. Let's explore how to build robust offline capabilities with Laravel and Flutter that'll keep your users productive regardless of their connection status.

The Offline-First Mindset

Before diving into implementation, let's establish the core principles:

  • Offline-First: Your app should work without a network connection as the default state, not as an exception. Users should be able to read, create, and modify data locally.

  • Eventual Consistency: Accept that data might be temporarily out of sync between client and server. Focus on graceful conflict resolution rather than preventing conflicts entirely.

  • Optimistic Updates: Update the UI immediately when users make changes, then sync with the server in the background. If conflicts arise, handle them gracefully.

  • Smart Synchronization: Only sync what's necessary, when it's necessary. Respect users' data plans and battery life.

Flutter: Building the Local Database Foundation

The key to offline functionality is having a robust local database. Flutter offers several excellent options, but I'll focus on the most practical approaches.

SQLite with Drift (Formerly Moor)

Drift is a powerful, type-safe SQLite wrapper that makes complex queries and migrations straightforward:

dependencies:
  drift: ^2.14.1
  sqlite3_flutter_libs: ^0.5.0
  path_provider: ^2.0.0
  path: ^1.8.0

dev_dependencies:
  drift_dev: ^2.14.1
  build_runner: ^2.3.0

Setting up your local schema:

// database.dart
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;

// Tables
class Tasks extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get title => text().withLength(min: 1, max: 200)();
  TextColumn get description => text().nullable()();
  BoolColumn get isCompleted => boolean().withDefault(const Constant(false))();
  DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
  DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
  
  // Sync fields
  IntColumn get serverId => integer().nullable()(); // Server-side ID
  BoolColumn get needsSync => boolean().withDefault(const Constant(true))();
  TextColumn get syncAction => text().nullable()(); // 'create', 'update', 'delete'
  DateTimeColumn get lastSyncAt => dateTime().nullable()();
}

@DriftDatabase(tables: [Tasks])
class AppDatabase extends _$AppDatabase {
  AppDatabase() : super(_openConnection());

  @override
  int get schemaVersion => 1;

  static LazyDatabase _openConnection() {
    return LazyDatabase(() async {
      final dbFolder = await getApplicationDocumentsDirectory();
      final file = File(p.join(dbFolder.path, 'app_database.db'));
      return NativeDatabase(file);
    });
  }
}

The Sync-Aware Repository Pattern:

class TaskRepository {
  final AppDatabase _db;
  
  TaskRepository(this._db);
  
  // Get all tasks (works offline)
  Stream<List<Task>> watchAllTasks() {
    return _db.select(_db.tasks).watch();
  }
  
  // Create task (works offline)
  Future<Task> createTask(String title, String? description) async {
    final task = TasksCompanion(
      title: Value(title),
      description: Value(description),
      needsSync: const Value(true),
      syncAction: const Value('create'),
    );
    
    final id = await _db.into(_db.tasks).insert(task);
    final createdTask = await (_db.select(_db.tasks)..where((t) => t.id.equals(id))).getSingle();
    
    // Trigger background sync
    _scheduleSync();
    
    return createdTask;
  }
  
  // Update task (works offline)
  Future<void> updateTask(Task task, {String? title, String? description, bool? isCompleted}) async {
    final update = TasksCompanion(
      id: Value(task.id),
      title: title != null ? Value(title) : const Value.absent(),
      description: description != null ? Value(description) : const Value.absent(),
      isCompleted: isCompleted != null ? Value(isCompleted) : const Value.absent(),
      updatedAt: Value(DateTime.now()),
      needsSync: const Value(true),
      syncAction: Value(task.serverId != null ? 'update' : 'create'),
    );
    
    await (_db.update(_db.tasks)..where((t) => t.id.equals(task.id))).write(update);
    _scheduleSync();
  }
  
  // Soft delete (works offline)
  Future<void> deleteTask(Task task) async {
    if (task.serverId != null) {
      // Mark for deletion sync
      await (_db.update(_db.tasks)..where((t) => t.id.equals(task.id))).write(
        const TasksCompanion(
          needsSync: Value(true),
          syncAction: Value('delete'),
        ),
      );
    } else {
      // Local-only task, delete immediately
      await (_db.delete(_db.tasks)..where((t) => t.id.equals(task.id))).go();
    }
    _scheduleSync();
  }
  
  void _scheduleSync() {
    // Trigger your sync service
    SyncService.instance.scheduleSync();
  }
}

Laravel: Building Sync-Friendly APIs

Your Laravel backend needs to support efficient synchronization. This means designing APIs that can handle batch operations, conflict resolution, and incremental updates.

Sync-Aware Models

Start by adding sync metadata to your Eloquent models:

// app/Models/Task.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Task extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'title',
        'description',
        'is_completed',
        'user_id',
    ];

    protected $casts = [
        'is_completed' => 'boolean',
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
        'deleted_at' => 'datetime',
    ];

    // Add a sync version for conflict resolution
    protected static function boot()
    {
        parent::boot();
        
        static::updating(function ($task) {
            $task->sync_version = ($task->sync_version ?? 0) + 1;
        });
    }
}

Migration for sync support:

// database/migrations/add_sync_fields_to_tasks_table.php
public function up()
{
    Schema::table('tasks', function (Blueprint $table) {
        $table->integer('sync_version')->default(1);
        $table->timestamp('client_updated_at')->nullable();
    });
}

Batch Sync Endpoints

Design your API to handle batch operations efficiently:

// app/Http/Controllers/SyncController.php
<?php

namespace App\Http\Controllers;

use App\Models\Task;
use Illuminate\Http\Request;

class SyncController extends Controller
{
    public function pullChanges(Request $request)
    {
        $lastSyncAt = $request->input('last_sync_at');
        $lastSyncAt = $lastSyncAt ? Carbon::parse($lastSyncAt) : null;

        $query = auth()->user()->tasks();

        if ($lastSyncAt) {
            $query->where(function ($q) use ($lastSyncAt) {
                $q->where('updated_at', '>', $lastSyncAt)
                  ->orWhere('deleted_at', '>', $lastSyncAt);
            });
        }

        $tasks = $query->withTrashed()->get();

        return response()->json([
            'tasks' => $tasks,
            'server_time' => now()->toISOString(),
        ]);
    }

    public function pushChanges(Request $request)
    {
        $changes = $request->input('changes', []);
        $results = [];

        foreach ($changes as $change) {
            $result = $this->processChange($change);
            $results[] = $result;
        }

        return response()->json([
            'results' => $results,
            'server_time' => now()->toISOString(),
        ]);
    }

    private function processChange(array $change)
    {
        $action = $change['action']; // 'create', 'update', 'delete'
        $clientId = $change['client_id'];
        $data = $change['data'];

        try {
            switch ($action) {
                case 'create':
                    $task = auth()->user()->tasks()->create($data);
                    return [
                        'client_id' => $clientId,
                        'status' => 'success',
                        'server_id' => $task->id,
                        'sync_version' => $task->sync_version,
                    ];

                case 'update':
                    $task = auth()->user()->tasks()->find($data['id']);
                    
                    if (!$task) {
                        return [
                            'client_id' => $clientId,
                            'status' => 'not_found',
                        ];
                    }

                    // Conflict detection
                    if (isset($data['sync_version']) && $task->sync_version > $data['sync_version']) {
                        return [
                            'client_id' => $clientId,
                            'status' => 'conflict',
                            'server_data' => $task->toArray(),
                        ];
                    }

                    $task->update($data);
                    
                    return [
                        'client_id' => $clientId,
                        'status' => 'success',
                        'sync_version' => $task->sync_version,
                    ];

                case 'delete':
                    $task = auth()->user()->tasks()->find($data['id']);
                    
                    if ($task) {
                        $task->delete();
                    }
                    
                    return [
                        'client_id' => $clientId,
                        'status' => 'success',
                    ];

                default:
                    return [
                        'client_id' => $clientId,
                        'status' => 'invalid_action',
                    ];
            }
        } catch (\Exception $e) {
            return [
                'client_id' => $clientId,
                'status' => 'error',
                'message' => $e->getMessage(),
            ];
        }
    }
}

The Synchronization Engine

The heart of your offline-first app is the sync service that orchestrates data flow between local storage and your server.

// sync_service.dart
import 'dart:async';
import 'dart:convert';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:http/http.dart' as http;

class SyncService {
  static final SyncService instance = SyncService._internal();
  SyncService._internal();

  final AppDatabase _db = AppDatabase();
  Timer? _syncTimer;
  bool _isSyncing = false;
  DateTime? _lastSyncAt;

  // Stream to notify UI about sync status
  final _syncStatusController = StreamController<SyncStatus>.broadcast();
  Stream<SyncStatus> get syncStatusStream => _syncStatusController.stream;

  void initialize() {
    // Listen for connectivity changes
    Connectivity().onConnectivityChanged.listen((result) {
      if (result != ConnectivityResult.none) {
        scheduleSync();
      }
    });

    // Periodic sync when online
    _syncTimer = Timer.periodic(const Duration(minutes: 5), (_) {
      scheduleSync();
    });
  }

  void scheduleSync() {
    if (_isSyncing) return;
    
    // Check connectivity first
    Connectivity().checkConnectivity().then((result) {
      if (result != ConnectivityResult.none) {
        _performSync();
      }
    });
  }

  Future<void> _performSync() async {
    if (_isSyncing) return;
    
    _isSyncing = true;
    _syncStatusController.add(SyncStatus.syncing);

    try {
      // Step 1: Pull changes from server
      await _pullFromServer();
      
      // Step 2: Push local changes to server  
      await _pushToServer();
      
      _lastSyncAt = DateTime.now();
      _syncStatusController.add(SyncStatus.success);
      
    } catch (e) {
      print('Sync failed: $e');
      _syncStatusController.add(SyncStatus.error);
    } finally {
      _isSyncing = false;
    }
  }

  Future<void> _pullFromServer() async {
    final response = await http.get(
      Uri.parse('${ApiConfig.baseUrl}/sync/pull'),
      headers: {
        'Authorization': 'Bearer ${await AuthService.getToken()}',
        'Content-Type': 'application/json',
      },
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      final serverTasks = data['tasks'] as List;

      for (final taskData in serverTasks) {
        await _mergeServerTask(taskData);
      }
    }
  }

  Future<void> _mergeServerTask(Map<String, dynamic> serverTask) async {
    final serverId = serverTask['id'];
    final isDeleted = serverTask['deleted_at'] != null;
    
    // Find existing local task by server ID
    final existingTask = await (_db.select(_db.tasks)
        ..where((t) => t.serverId.equals(serverId)))
        .getSingleOrNull();

    if (isDeleted) {
      // Handle server deletion
      if (existingTask != null) {
        await (_db.delete(_db.tasks)..where((t) => t.id.equals(existingTask.id))).go();
      }
      return;
    }

    if (existingTask != null) {
      // Update existing task (server wins for now - you could implement smarter conflict resolution)
      await (_db.update(_db.tasks)..where((t) => t.id.equals(existingTask.id))).write(
        TasksCompanion(
          title: Value(serverTask['title']),
          description: Value(serverTask['description']),
          isCompleted: Value(serverTask['is_completed']),
          updatedAt: Value(DateTime.parse(serverTask['updated_at'])),
          needsSync: const Value(false),
          lastSyncAt: Value(DateTime.now()),
        ),
      );
    } else {
      // Create new task from server
      await _db.into(_db.tasks).insert(TasksCompanion(
        serverId: Value(serverId),
        title: Value(serverTask['title']),
        description: Value(serverTask['description']),
        isCompleted: Value(serverTask['is_completed']),
        createdAt: Value(DateTime.parse(serverTask['created_at'])),
        updatedAt: Value(DateTime.parse(serverTask['updated_at'])),
        needsSync: const Value(false),
        lastSyncAt: Value(DateTime.now()),
      ));
    }
  }

  Future<void> _pushToServer() async {
    // Get all items that need syncing
    final itemsToSync = await (_db.select(_db.tasks)
        ..where((t) => t.needsSync.equals(true)))
        .get();

    if (itemsToSync.isEmpty) return;

    final changes = itemsToSync.map((task) => {
      'client_id': task.id,
      'action': task.syncAction ?? 'update',
      'data': {
        if (task.serverId != null) 'id': task.serverId,
        'title': task.title,
        'description': task.description,
        'is_completed': task.isCompleted,
        'client_updated_at': task.updatedAt.toIso8601String(),
      },
    }).toList();

    final response = await http.post(
      Uri.parse('${ApiConfig.baseUrl}/sync/push'),
      headers: {
        'Authorization': 'Bearer ${await AuthService.getToken()}',
        'Content-Type': 'application/json',
      },
      body: jsonEncode({'changes': changes}),
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      final results = data['results'] as List;

      for (final result in results) {
        await _handleSyncResult(result);
      }
    }
  }

  Future<void> _handleSyncResult(Map<String, dynamic> result) async {
    final clientId = result['client_id'];
    final status = result['status'];

    final task = await (_db.select(_db.tasks)
        ..where((t) => t.id.equals(clientId)))
        .getSingleOrNull();

    if (task == null) return;

    switch (status) {
      case 'success':
        // Update with server ID if it's a new item
        final serverId = result['server_id'];
        await (_db.update(_db.tasks)..where((t) => t.id.equals(clientId))).write(
          TasksCompanion(
            serverId: serverId != null ? Value(serverId) : const Value.absent(),
            needsSync: const Value(false),
            syncAction: const Value.absent(),
            lastSyncAt: Value(DateTime.now()),
          ),
        );
        break;

      case 'conflict':
        // Handle conflict - for now, server wins, but you could present UI for user to resolve
        final serverData = result['server_data'];
        await _mergeServerTask(serverData);
        break;

      case 'not_found':
        // Item doesn't exist on server, might have been deleted
        await (_db.delete(_db.tasks)..where((t) => t.id.equals(clientId))).go();
        break;

      case 'error':
        // Keep for retry - could implement exponential backoff
        print('Sync error for item $clientId: ${result['message']}');
        break;
    }
  }

  void dispose() {
    _syncTimer?.cancel();
    _syncStatusController.close();
  }
}

enum SyncStatus { idle, syncing, success, error }
```.fromJson(messageData)));
    });
  }
  
  void _onMessageReceived(MessageReceived event, Emitter<ChatState> emit) {
    // Update state with new message
  }
}

Production Considerations

  • Connection Management: Handle reconnections gracefully. Network conditions change, especially on mobile.
  • Rate Limiting: Don't overwhelm your server or users with too many updates.
  • Authentication: Secure your WebSocket connections. Use tokens that can expire and be refreshed.
  • Scaling: Consider using Redis for horizontal scaling of WebSocket connections.
  • Fallback Strategies: Have polling as a fallback for environments where WebSockets are blocked.

The Pragmatic Approach

Real-time features can make your app feel magical, but they also add complexity. Start simple:

  1. Identify Real Needs: Not every update needs to be real-time. Sometimes “eventual consistency” is perfectly fine.
  2. Start with SSE: If you primarily need server-to-client updates, SSE is simpler than full WebSockets.
  3. Use Hosted Solutions Initially: Pusher or Ably can get you moving quickly. You can always self-host later.
  4. Test Network Conditions: Real-time features behave differently on poor connections. Test accordingly.

Real-time communication transforms user experiences, making apps feel responsive and alive. Both Laravel and Flutter provide excellent tools to make this happen smoothly.

What real-time features are you planning to build? Any challenges you've faced with WebSockets or SSE? Let's chat about it!

Cheers,

Jamie C

 
Read more...

from The Pragmatic Pixel

Hey everyone, Jamie here.

So, your app is humming along, features are shipping, and users are happy. But what if those users span different countries and speak different languages? Suddenly, “Your order has been placed!” needs to be “¡Tu pedido ha sido realizado!” or “Votre commande a été passée!”. This is where Internationalization (i18n) and Localization (l10n) come into play – crucial steps if you're aiming for a global audience with your Laravel + Flutter application.

It might seem daunting, but both Laravel and Flutter offer excellent tools to make this process manageable. Let's break down how to approach it.

Understanding the Terms

First, a quick refresher:

  • Internationalization (i18n): Designing and developing your application so it can be adapted to various languages and regions without engineering changes. Think of it as building a “language-agnostic” foundation. This includes things like using Unicode, supporting right-to-left (RTL) text, and externalizing strings.
  • Localization (l10n): The process of actually adapting your internationalized application for a specific region or language by adding locale-specific components and translating text. This includes translating UI strings, formatting dates, times, numbers, and currencies according to local conventions.

You do i18n first, so that l10n becomes easier.

Laravel: Handling Translations and Locale on the Backend

Our Laravel API plays a key role, especially if some content or messages originate from the server.

  1. Language Files: Laravel's localization features are primarily driven by language files stored in the lang directory (or resources/lang in older versions).

    • You'll create subdirectories for each supported language (e.g., en, es, fr).
    • Inside these, you'll have PHP files (e.g., messages.php) or JSON files (e.g., es.json) that return an array of keyed strings.

    Example (lang/es/messages.php):

    <?php
    
    return [
        'welcome' => '¡Bienvenido a nuestra aplicación!',
        'profile_updated' => 'Perfil actualizado con éxito.',
    ];
    

    Example (lang/fr.json):

    {
        "welcome": "Bienvenue sur notre application !",
        "profile_updated": "Profil mis à jour avec succès."
    }
    
  2. Retrieving Translated Strings:

    • You use the __('key') helper function or the @lang('key') Blade directive to retrieve translated strings. php echo __('messages.welcome'); // In PHP // {{ __('messages.welcome') }} or @lang('messages.welcome') in Blade
    • For JSON files, you just use the key: __('Welcome to our application!') if your default locale is en and you have an en.json with that key, and then a corresponding key in es.json.
  3. Pluralization: Laravel handles pluralization elegantly using a | character to separate singular and plural forms, and you can define more complex pluralization rules.

    // 'item_count' => 'There is one item|There are :count items'
    echo trans_choice('messages.item_count', 5); // Output: There are 5 items
    
  4. Setting the Locale:

    • The application's locale is set in config/app.php (locale and fallback_locale).
    • You can change the locale at runtime using App::setLocale('es');.
    • Commonly, you'd determine the user's preferred locale from:
      • A user profile setting stored in the database.
      • The Accept-Language HTTP header sent by the browser/client.
      • A segment in the URL (e.g., /es/dashboard).
    • A middleware is often used to set the locale for each request based on these factors.
  5. API Responses: If your API needs to return localized messages (e.g., validation errors, success messages), Laravel's default validation messages and notifications can also be translated by publishing their language files and adding your translations.

Flutter: Building a Multilingual UI

Flutter has excellent built-in support for i18n and l10n, primarily through the flutter_localizations package and code generation for message catalogs.

  1. Dependencies: Add flutter_localizations to your pubspec.yaml and potentially intl for more complex formatting.

    dependencies:
      flutter:
        sdk: flutter
      flutter_localizations: # Add this
        sdk: flutter         # Add this
      intl: ^0.18.0 # Or latest, for formatting and message extraction
        
    flutter:
      uses-material-design: true
      generate: true # Important for code generation
    
  2. Configuration:

    • In your MaterialApp (or CupertinoApp), specify localizationsDelegates and supportedLocales.

      import 'package:flutter_localizations/flutter_localizations.dart';
      // Import your generated AppLocalizations class (see below)
      // import 'generated/l10n.dart';
      
      MaterialApp(
        // ... other properties
        // localizationsDelegates: AppLocalizations.localizationsDelegates, // Generated
        // supportedLocales: AppLocalizations.supportedLocales, // Generated
        localizationsDelegates: [
          // AppLocalizations.delegate, // Your app's generated delegate
          GlobalMaterialLocalizations.delegate,
          GlobalWidgetsLocalizations.delegate,
          GlobalCupertinoLocalizations.delegate,
        ],
        supportedLocales: [
          const Locale('en', ''), // English, no country code
          const Locale('es', ''), // Spanish, no country code
          const Locale('fr', ''), // French, no country code
          // ... other locales your app supports
        ],
        // locale: _userLocale, // Optionally set the initial locale
        // localeResolutionCallback: (locale, supportedLocales) { ... } // For custom logic
      );
      
  3. ARB Files (.arb): Application Resource Bundle files are used to store your translated strings. You'll typically have one per locale (e.g., app_en.arb, app_es.arb). These are usually placed in an l10n directory at the root of your project.

    Example (l10n/app_en.arb):

    {
      "helloWorld": "Hello World!",
      "welcomeMessage": "Welcome {userName} to our awesome app!",
      "@welcomeMessage": {
        "description": "A welcome message shown on the home screen",
        "placeholders": {
          "userName": {
            "type": "String",
            "example": "Jamie"
          }
        }
      },
      "itemCount": "{count,plural, =0{No items}=1{One item}other{{count} items}}",
      "@itemCount": {
        "description": "Indicates the number of items",
        "placeholders": {
          "count": {
            "type": "int"
          }
        }
      }
    }
    

    (And a corresponding app_es.arb, app_fr.arb etc.)

  4. Code Generation: Flutter tools use the .arb files to generate Dart code that provides access to your localized strings.

    • Ensure generate: true is in your pubspec.yaml under the flutter section.
    • Running flutter pub get (or building your app) will trigger code generation (usually into lib/generated/l10n.dart).
  5. Using Localized Strings in Widgets:

    • Import the generated localizations class (often AppLocalizations).
    • Access strings via AppLocalizations.of(context)!.yourStringKey.
    // import 'generated/l10n.dart'; // Your generated file
    
    class MyWidget extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // final l10n = AppLocalizations.of(context)!; // Get the localizations instance
        return Scaffold(
          // appBar: AppBar(title: Text(l10n.helloWorld)),
          // body: Center(child: Text(l10n.welcomeMessage('Jamie'))),
          appBar: AppBar(title: Text("Example Title")), // Placeholder until l10n is fully set up
          body: Center(child: Text("Welcome Jamie")), // Placeholder
        );
      }
    }
    
  6. Formatting Dates, Numbers, Currencies: Use the intl package for locale-aware formatting.

    // import 'package:intl/intl.dart';
    // DateFormat.yMMMd(AppLocalizations.of(context)!.localeName).format(DateTime.now());
    // NumberFormat.currency(locale: AppLocalizations.of(context)!.localeName, symbol: '€').format(123.45);
    
  7. Changing Locale Dynamically: You'll need a way for users to select their language, or detect it. This usually involves a state management solution (Provider, Riverpod, Bloc) to hold the current Locale and rebuild MaterialApp when it changes.

Syncing Backend and Frontend Locales

  • When your Flutter app makes API calls to Laravel, you might want to include the current app locale in a header (e.g., X-App-Locale: es).
  • Your Laravel middleware can then use this header to set the backend locale for that request, ensuring any API responses (like validation messages) are also localized.

Key Considerations

  • Translation Management: For larger apps, managing .arb or PHP language files manually can be cumbersome. Consider using translation management platforms (e.g., Lokalise, Phrase, Crowdin) that can often export in the required formats.
  • Right-to-Left (RTL) Support: If you support languages like Arabic or Hebrew, ensure your UI correctly handles RTL layouts. Flutter's Directionality widget and Material/Cupertino widgets often handle this well if the locale indicates an RTL language.
  • Testing: Test all supported languages and regions thoroughly. Pay attention to UI overflows due to varying string lengths.
  • Context is Key for Translators: Provide context (screenshots, descriptions like in @key in ARB files) to translators so they understand where and how strings are used.

The Pragmatic Path

Going global is an investment, but it opens your app to a much wider audience.

  1. Internationalize Early: Design with i18n in mind from the start (externalize strings, think about layout).
  2. Start with Key Languages: You don't need to support every language on day one. Begin with your primary target markets.
  3. Leverage Framework Tools: Both Laravel and Flutter provide robust localization systems. Learn and use them.
  4. Automate Where Possible: Use code generation in Flutter and consider translation management tools for larger projects.

Taking your application multilingual can seem like a big step, but by breaking it down and utilizing the powerful features within Laravel and Flutter, you can create a truly global experience for your users.

Have you tackled i18n/l10n in your projects? Any tips or pitfalls to share? Let's discuss!

Cheers,

Jamie C

 
Read more...

from The Pragmatic Pixel

Hey everyone, Jamie here.

We've journeyed through API design, state management, deployment, error monitoring, and configuration. But where does all this code actually get written and tested before it sees the light of day? That's right, our trusty local development environment.

Setting up a smooth, efficient local environment when you're juggling a Laravel backend and a Flutter frontend can sometimes feel like a bit of a dark art. You need PHP, Node.js, Composer, the Flutter SDK, Dart, maybe a database server, emulators, and a code editor that plays nicely with all of it. A clunky or inconsistent local setup can be a major drag on productivity and a source of constant frustration.

So, let's talk about crafting a “dev cave” that makes working across both stacks as painless and productive as possible.

Part 1: Taming the Backend (Laravel Local Development)

For Laravel, the goal is to have a consistent PHP environment with all necessary extensions, a database, and tools like Composer readily available.

  1. Laravel Sail (The Modern Default):

    • What it is: Sail is Laravel's official Docker-based development environment. It provides a pre-configured docker-compose.yml file that spins up containers for PHP, your chosen database (MySQL, PostgreSQL, etc.), Redis, MeiliSearch, Mailpit, and more.
    • Why it's great:
      • Consistency: Everyone on the team (or just your different machines) runs the exact same environment, eliminating “it works on my machine” issues.
      • Simplicity: sail up and you're running. sail artisan ..., sail composer ..., sail npm ... commands run inside the Docker containers.
      • No Local PHP/DB Installation: You don't need to install PHP, MySQL, or Redis directly on your host machine (just Docker Desktop).
    • Getting Started: New Laravel projects often come with Sail. For existing ones, it's easy to add.
  2. Other Options (Still Valid, But Sail is Gaining):

    • Laravel Valet (macOS only): Super lightweight, serves sites via Nginx, uses local PHP installations. Great if you're exclusively on macOS and prefer not to use Docker for PHP dev.
    • Laravel Homestead (Vagrant): A pre-packaged Vagrant box. More heavyweight than Sail but very robust and provides a full Ubuntu VM.
    • Manual Setups (XAMPP, MAMP, WAMP, Homebrew PHP): Installing PHP, a web server, and a database directly on your OS. Gives you full control but also full responsibility for configuration and potential conflicts.
  3. Essential Backend Tools:

    • Code Editor: VS Code with extensions like “PHP Intelephense” (or “PHP All-in-One”), “Laravel Extension Pack,” and “DotENV” is a popular choice. PhpStorm is a powerful paid IDE.
    • Database GUI: TablePlus, DBeaver, Sequel Ace (macOS), or MySQL Workbench make database interaction much easier.
    • API Client: Postman, Insomnia, or even VS Code's Thunder Client extension for testing your API endpoints directly.

Part 2: Powering the Frontend (Flutter Local Development)

For Flutter, you need the Flutter SDK, Dart, and ways to run your app.

  1. Flutter SDK: Download from the official Flutter website and add it to your system's PATH. Run flutter doctor to ensure everything is set up correctly and to install any missing dependencies (like Android SDK tools or Xcode command-line tools).

  2. IDE/Editor:

    • VS Code: Excellent Flutter support with the official “Flutter” and “Dart” extensions. Provides great debugging, hot reload/restart, and widget inspection tools.
    • Android Studio (or IntelliJ IDEA with Flutter plugin): Also provides a first-class Flutter development experience, with more integrated Android-specific tooling.
  3. Running Your App:

    • Emulators/Simulators:
      • Android Emulator: Set up via Android Studio's AVD Manager.
      • iOS Simulator (macOS only): Comes with Xcode.
      • Pros: Convenient, good for most UI testing.
      • Cons: Can be resource-intensive. Don't always perfectly replicate real device behavior or performance.
    • Physical Devices:
      • Pros: The most accurate way to test performance, native integrations (camera, GPS), and gestures. Essential before release.
      • Cons: Requires enabling developer mode, USB debugging/device provisioning.

Part 3: Making Laravel and Flutter Talk Locally

This is where networking nuances come in. Your Flutter app running on an emulator or physical device needs to be able to reach your Laravel API running on your host machine.

  • Laravel API URL: Typically http://localhost:8000 (or whatever port Sail/Valet/etc. uses).
  • Flutter App Accessing Host localhost:
    • Android Emulator: Uses the special IP http://10.0.2.2 to refer to your host machine's localhost. So, your Flutter app's API base URL for Android dev would be http://10.0.2.2:8000.
    • iOS Simulator: Can usually use http://localhost:8000 directly.
    • Physical Device (on same Wi-Fi): You'll need to use your host machine's local network IP address (e.g., http://192.168.1.100:8000). Find this via ipconfig (Windows) or ifconfig (macOS/Linux). Ensure your firewall allows incoming connections to that port.
  • Tools for Temporary Public URLs (if needed):
    • If you need to test webhooks from a third-party service to your local Laravel instance, or test on a physical device not on your local Wi-Fi, tools like ngrok or Expose (by BeyondCode) can create a temporary public URL that tunnels to your local server.

Tips for a Smooth Workflow

  • Consistent Tooling: Using the same editor (like VS Code) for both Laravel (PHP) and Flutter (Dart) can streamline context switching.
  • Leverage Hot Reload/Restart: Flutter's hot reload (for UI changes) and hot restart (for state changes) are game-changers for rapid iteration.
  • Dependency Management: Regularly run composer update (Laravel) and flutter pub get / flutter pub upgrade (Flutter) to keep dependencies in check, but be mindful of breaking changes.
  • Version Control Everything (Almost): Use Git. Commit often. But remember .env files (Laravel) and potentially generated build files should be in your .gitignore.

The Goal: Focus on Building

A well-oiled local development environment should fade into the background, allowing you to focus on what really matters: building awesome features for your users. While the initial setup can take a bit of time, the investment pays off daily in increased productivity and reduced frustration.

What are your must-have tools or tricks for your Laravel + Flutter local dev setup? Share your wisdom in the comments!

Cheers,

Jamie C

 
Read more...

from Virtus Computing

This tutorial continues from the last one in the NotePad app set Basic4Android: Notepad – Part1 (Designing). In this tutorial i am taking the design and adding code to make the app load the text from files and display it in one of the four slots.

App start up

When the app starts up we are going to load in the design with the buttons and the text box in. We are also going to put things into the menu. All of this start up code goes inside the Activity create sub.

Sub Activity_Create(FirstTime As Boolean)
    'Code to run here
End Sub

Loading the layout

First of all loading the layout, all this is done with one line of code which just loads the file we saved in the first tutorial. We made main.bal in the first tutorial and now we are displaying that.

Activity.LoadLayout("main.bal")

Adding menu items

Adding menu items is very simple, we are going to add three menu items Clear, Save, Exit. These will only appear when the menu button is pressed so it saves on screen space. It is one line of code for each menu item. We are only using text items so the line of code is this:

Activity.AddMenuItem("title", "event")

The title is what you want the menu button to say example “Clear” and the event is where you want to run the code example “clear” which would run the sub “clear_Click”. We are adding the three menu items below:

	Activity.AddMenuItem("Clear", "clear")
	Activity.AddMenuItem("Save", "save")
	Activity.AddMenuItem("Exit", "exit")

Loading the slots

When each slot button is pressed two things will happen.

  • The data for that slot will be loaded and displayed.
  • The strings will be changed so that when saved the data is saved to the right slot. We will do the second one first as it is the simplest to do and requires only a few lines of code. First we need to set up the variable to store which slot is open. To do this we Dim the string in the Globals sub. Inside there under the setting up of the buttons and text box add the two lines. Dim slot As String slot = "0" That line lets the program know that if slot is used that it is a bit of data. Right under that line we are going to set slot to 0. This is because we don’t want bugs when a user clicks save straight away. Now we need to put a bit of code into each button so that the slot string changes. For example the button for slot 1 will be: Sub slot1_Click slot = "1" End Sub This needs to be added to all of the other subs, they should have been generated in the first tutorial, so all you need to do is add the second line to each sub. Remember to change the number so that slot2_Click contains slot = “2″ and so on.

Now we need to load the data for each slot. This is very simple to do we just need to load a text file when each button is clicked. this is done in one line, but we are going to make sure the file is there before opening to stop any errors. So under the line above we need to check for a file like this:

If File.Exists(File.DirInternal,"notepadapp-slot" & slot & ".txt") Then
    	notepadbox.Text = File.ReadString(File.DirInternal, "notepadapp-slot" & slot & ".txt")
	ToastMessageShow("Data loaded for slot " & slot & ".", True)
Else
	notepadbox.Text = "There is no data for slot" & slot
	ToastMessageShow("No data loaded for slot " & slot & ".", True)
End If

That code checks to see if the file notepadapp-slow1.txt is there, if it is then it will load the data into the text box. If not then it will display a message. The slot can still be loaded without there being data, and when it is saved for the first time it will create that file for the next time. That code needs to be put in all the four slot buttons and does not need to be modified.

Saving the slots

saving the slots is very easy, it is again just one line of code to do so. You will need to create a new sub like below with the following code inside. It can be anywhere in the code i added it at the bottom.

Sub save_click
	File.WriteString(File.DirInternal, "notepadapp-slot" & slot & ".txt", notepadbox.Text)
	ToastMessageShow("Data saved for slot " & slot & ".", True)
End Sub

The Clear button

The clear button just resets the files to default and removes all the data from it, it is only a few lines of code. it sets the text box to having no data and then writes that to the file. A new sub is need for the clear button just like with save.

Sub clear_click
	notepadbox.Text = "There is no data for slot" & slot
	File.WriteString(File.DirInternal, "notepadapp-slot" & slot & ".txt", notepadbox.Text)
	ToastMessageShow("Data deleted for slot " & slot & ".", True)
End Sub

The exit button

The last button in the menu is the exit button, this is to fully stop the program and not leave it running in the background. I like this in my apps so i can close them and speed up my phone and save battery. The code is very simple and uses a new sub.

Sub exit_Click
	Activity.Finish
	ExitApplication
End Sub

Finishing off

The app now fully works, there is one last thing i want to do to make it work better across phones with big screens. There are many things that can be edited along with colors designs, and loading the slots in a list box. This is the trick i used to make it better on bigger phones it just makes the text box adjust to the screen size, this code goes right after the menu code near the top.

notepadbox.Width = Activity.Width
notepadbox.Height = (Activity.Height - 50)

Done

That is now the app working fully. There is many things that can be added and made better, but this is a working android program that uses files to store data.

Screenshots

notepadappscreenshot (1) notepadappscreenshot (2) notepadappscreenshot (3)

End result

The app on Google Play

Written by Zachary.

 
Read more...

from Virtus Computing

In this set of tutorials i will go through how to make a notepad app. A very simple one with 4 slots and a text box to edit the text in them slots. It will save the data on the SD card if there is one and on the phone if not. I will have settings to change fonts and colours for each slot. This tutorial will be split into 2 parts:

  • Designing
  • Saving and loading slots

After that i will go on to add more functions and explain more things that can be done in B4A and how to use them.

Application settings.

First of all open up B4A and click save, this is because you can not use the GUI designer without saving it first. I recamend making a new folder to save it in like NotepadApp. Put the file name as notepad and click save. Saving the Notepad app.

When it is saved we can start to set up the settings of the app, Like the Name, Icon and Version number. Got to Project and select Choose Icon. Then select the icon from your computer. I am going to use a simple one i found on the internet. (You can download it here)

The next thing we want to edit is Package Name, this is a unique field that identifies your app and will be different from all other apps. Most of the time Developers put this to:

*appname*.*developername*.*companyname*

That is because it has to conation at least two words with a dot between them, it must all be lowercase. In this tutorial i am going to set mine to notepad.virtuscomputing.tutorial. You may use that name as well.

The last one we are going to change today is the Application Label, This is the name of your app and the one that is displayed on the app draw, in your settings, and on the top of the app. I set mine to NotePad.

Designing.

Now we can open up the designer and start to build the GUI. It will be one big Text-box with a tab host along the top. There will be a menu with a Save, Clear and Exit buttons on it. This will be most of the app interface for now. Open up the designer and click Add View then Edit Text, Drag it around the form with the square in the corners. It needs to fit the entire screen, starting from 50 from the top going all the way down to the bottom. You can then add 4 buttons and spread them across the gap left in the top. To make our code easy to read and understand rename the buttons to slot1, slot2, slot3, slot4. And the textbox to notepadbox. This is done by selecting the item, and looking at the top of the list of propities and changing the Name field.

Notepad Design Form

You should end up with something like the form above. At this point you can move thigs around to how you want them to be. Then click File –> Save, and name it main. Once this is done there is one more thing we need to do before starting to code the app. Select Tools –> Generate Members. When the new form opens up Select all views to select all the boxes. On slot1, slot2, slot3, and slot4 open then with the little + to one side and select the box for Click. Once this is done for all of them press Generate Members.

Notepad Generating Members

This inserts all the code into the file to make all them forms appear That is the last of the designing for now. You can move things around in the designer and make them look how you want.

All the code is in Basic4Android: Notepad – Part2 (Code)

Written by Zachary.

 
Read more...

from Virtus Computing

To make the test application we are just going to do the hello world app. This just prints the words hello world on the form to prove that it works. When you clickt he text hello world it will change, just to show how events are handled.

To start when you open Basic4Android it gives you a set of code already. We will be using this code to click Save and select where you want to save the project and all its files. I recommend making a new folder just for the project. Then name the project what you want. here is it called Testing for Tutorial then click save.

After we have save the project the file is created with a .b4a on the end (as seen in the picture above). Once we have saved we can now open up the designer, Click Designer on the menu bar. Two new windows will open up like the below. One of them (The bigger one) Is the control properties window. The other is the Designer.

You can now click Add New and then Label. It inserts the label onto the form and called it Label1 You can drag this around and re size it if you want to (When the item is clicked on the properties window you can change the test font and size. You can input the text there, but we are going to do it programmatic for now. Right click on the label and hover over Generate then Dim Label1 As Label, Then do that again but select Click this time.

Once that has been done we can save the layout, Click File then Save and give the layout a name of main and click Ok. The window should look like the one below.

Once we have done that we can close the designer and begin on the code. There is not allot of code for this program. The first bit it to load the label one we start the program. And then to print text into the label.

Sub Activity_Create(FirstTime AsBoolean)
Activity.LoadLayout("main")
'The line above is to load in the layout we made, remember the name "main"
Label1.Text = "Hello World"
'The line above print the text "Hello World" onto label1
End Sub

The code above should replace the code already in the Activity_Create. The code below is to change the text when the user clicks on the label.

Sub Label1_Click
Label1.Text = "I've been clicked"
'The line above print the text "I've been clicked" onto label1
End Sub

The code above should replace whats in Label1_Click. When the activity is started it calls the first bit of code which sets up the windows and loads in the forms. It then prints the first bit of text to the label. When the label is clicked it calls the second bit of code which changes the text. Make sure to save at this point!

Testing on a android device

If you are using a physical android device then connect it to the same WiFi to the computer(this is the way i do it) Download the Program to your phone (The Basic4Android Bridge linked in the top) Then run it, it needs to configuration and should have some buttons on there. Click the button that says Start – Wireless, it should then start the service. It will say “waiting for connections” when it is ready. Then go to your computer and in basic for android go to Tools then B4A Bridge then Connect – Wireless.

It will prompt you for an IP, This is displayed on your phone and should be something like 192.168.1.68. Type it in and click connect.

If it has worked your phone will say connected.

If it has worked then you can now click the Blue Arrow on basic for android and it will compile the application. When it is done your device will prompt you to install the app automatically. But the APK file is in /objects/Testing for Tutorial.apk so you can send it to your device another way.

Installing the app on the phone. You need to click Install at the first screen, It will then install the application

After it has installed you can Open or Done, You want to open it and make sure it has worked. If you click the label the text will change.

Make sure to press Stop on B4A Bridge to save your battery when you are done.

Written by Zachary.

 
Read more...

from Virtus Computing

Basic4Android is a very useful program to write applications for the android operating system. It runs on windows and has a very similar feel to VB.net. The IDE is very clean and easy to use. And with the remote bridge software you can install the applications straight to a mobile device.

Getting all the files.

Java JDK 7 (32-bit or 64-bit)

http://www.oracle.com/technetwork/java/javase/downloads/jdk7u9-downloads-1859576.html

Android SDK

http://dl.google.com/android/installer_r20.0.3-windows.exe

Basic4Android trail setup (If you have bought it then use the setup sent in the email)

Enterprise Standard (Trial)

Basic4Android Bridge (From the market onto your phone – Optional)

https://play.google.com/store/apps/details?id=anywheresoftware.b4a.b4abridge (http://vcurl.co/?i=f2ea)

Installing

Java

Installing java first as everything needs it. To install java i find it is best to uninstall any Java things you have previously installed. That way you are starting from scratch. Once it has been uninstalled run the Java setup program and install Java to the default place. (The Java JDK development tools will still work for any other java resource. including your browser) Once the Java install has completed do not restart your computer.

Android SDK

Installing the android SDK is a little more complicated. Run the setup and follow through the instructions. Install it all to the default place after the install has completed go to your Start menu and find the “SDK Manager” run that and wait while it starts up (This can take some time). Once it has started up wait for it to refresh the package lists. One it has done select. Android 4.1 (And all that’s inside it) Then click Install packages… You will have to select Agree and Install. Then the bar at the bottom will show the progress of the installation.

Basic4Android

One everything else is installed, i recommend you have restarted before installing Basic4Android. If you are installing the trail then you need to run the file you downloaded at the top. If you have bought it then download the file from your emails and download that file to install. When running the file just install the the default places.

Configuration

When all programs are installed, it is time to configure Basic4Android to work with the Android SDK, and Java JRE. Doing this is very simple and it is only inputting two file paths. One for the javaac.exe and the other for android.jar. To do this open up Basic4Android, Once it has loaded (you will have to select Trail, unless you have a key in which case select the key). Go to Tools then Configure Paths

After you click that you are presented with the window below. You need to locate the two files it says about. It has a location of where they should be but i found that it is not there anymore. Click browse to find the files. Which for me are in these locations (Just remember to change to your username, and correct versions.)

C:\Program Files\Java\jdk1.7.0_07\bin\javac.exe

C:\Users\Zachary Claret-Scott\AppData\Local\Android\android-sdk\platforms\android-16\android.jar

The Additional Libraries folder is if you want to specify an extra folder for it to look for them. It already looks in the program files for them and that’s were we will place them. I will cover libraries in other posts but they allow you to do more stuff like tracking the usages of an app, or talk to ftp.

In the next Guide i will Talk about how to make a application. (Here)

Written by Zachary.

 
Read more...

from keleven

Hopefully this blog will chart my motorbike rides on my trusty Royal Enfield 350 Classic.

I might add other things that I find interesting, then I might not.

 
Read more...

from Zachary Claret-Scott

#music #spotify #lastfm #data

I’ve been tracking my music listening habits for decades on last.fm ever since my dad introduced me to the platform – it’s great to see what music I listened to overtime.

I used to use last.fm daily to find new music by exploring radios, in a time before streaming when ripping mp3s of disk (or online) was king it was the best way to find new music. Once Spotify took over I dumped my local music collection and exclusively listened to streamed music, I had my last.fm connected and mostly ignored it while it silently collected my data.

I’m not sure when last.fm changed but it’s a far throw from its glory days now and it’s hard to see really simple stats about my listening history without signing up for a pro membership. Recently I found an alternative which is exclusive to Spotify called YourSpotify. It is self hosted and pulls recent listening data in real time from the Spotify API but requires a data export from Spotify (which takes up to 30 days) for full historic listening data.

Cover Image

 
Read more...

from Zachary Claret-Scott

#services #selfhosted #homelab

I have wanted to host my own fediverse/activity pub compatible server for a while – however I felt running a full on Mastodon server was to heavy for my use case. And other options seemed to take a bit too much effort to host for my liking.

I had all but given up until I came across Write Freely which is a blog focused platform that just so happens to share posts via activity pub. Setting it up was super simple as a single docker container and it supports single user mode, or multi user mode with very basic permissions.

I have put it up on a spare domain which I can use as my federated username @prra.xyz because it is generic I can also use it as a social media post engine for all my company accounts.

Cover Image

 
Read more...

from Zachary Claret-Scott

#services #selfhosted #jobs #career #softwaredeveloper

I am due to start a new role next week which I am very excited for, but before I confirmed I had to go through the process of trying to find roles available in my industry and area. While sites like Indeed, and LinkedIn have vast amount of job postings I found them difficult to navigate and often out of date – or companies straight up don't post to them.

I have a list of local companies or companies in my ideal sector which I would like to work for, you could call this my list of dream jobs. It includes some interesting tech startups nearby, charities I like the mission off etc.. Most of these organisations have a careers page which I can visit to see any open roles, but it's hard work checking 15 or so job pages every few days to see if anything has opened up.

Thats where some self hosted software came into play, I setup my own instance of Change Detection and loaded in all of the companies I wanted to track. Now twice a day I get an email if any of them post new job listings, even if they don't post those roles to large job sites.

Cover Image

 
Read more...