Flutter ile kelime bulmacası oluşturun

1. Başlamadan önce

Dünyanın en büyük bulmacasını oluşturmanın mümkün olup olmadığını sorduğunuzu düşünün. Okulda öğrendiğiniz bazı yapay zeka tekniklerini hatırlıyor ve hesaplama açısından yoğun sorunlara çözüm oluşturmak için algoritmik seçenekleri keşfetmek üzere Flutter'ı kullanıp kullanamayacağınızı merak ediyorsunuz.

Bu codelab'de tam olarak bunu yapacaksınız. Kursun sonunda, kelime bulmacaları oluşturmak için kullanılan algoritmalarla ilgili bir araç geliştirirsiniz. Geçerli bir bulmacanın tanımı değişiklik gösterir. Bu teknikler, kendi tanımınıza uygun bulmacalar oluşturmanıza yardımcı olur.

Oluşturulan bir bulmacanın animasyonu.

Bu aracı temel alarak, kullanıcının çözebileceği bir bulmaca oluşturmak için bulmaca oluşturucuyu kullanarak bir bulmaca hazırlarsınız. Bu bulmaca Android, iOS, Windows, macOS ve Linux'ta kullanılabilir. Android'de oynatıyorum:

Pixel Fold emülatöründe çözülmekte olan bir bulmacanın ekran görüntüsü.

Ön koşullar

Öğrenecekleriniz

  • Flutter'ın compute işlevi ve Riverpod'un select yeniden oluşturma filtresinin değer önbelleğe alma özelliklerinin bir kombinasyonuyla, Flutter'ın oluşturma döngüsünü engellemeden hesaplama açısından maliyetli işleri yapmak için izoleleri kullanma.
  • Derinlik öncelikli arama ve geri izleme gibi arama tabanlı Good Old Fashioned AI (GOFAI) tekniklerini uygulamak için built_value ve built_collection ile değişmez veri yapılarından nasıl yararlanılır?
  • Izgara verilerini hızlı ve sezgisel bir şekilde görüntülemek için two_dimensional_scrollables paketinin özelliklerini kullanma.

İhtiyacınız olanlar

  • Flutter SDK
  • Flutter ve Dart eklentileri ile Visual Studio Code (VS Code)
  • Seçtiğiniz geliştirme hedefi için derleyici yazılımı. Bu codelab, tüm masaüstü platformlarında, Android'de ve iOS'te çalışır. Windows'u hedeflemek için VS Code, macOS veya iOS'i hedeflemek için Xcode ve Android'i hedeflemek için Android Studio'ya ihtiyacınız vardır.

2. Proje oluşturma

İlk Flutter projenizi oluşturma

  1. VS Code'u başlatın.
  2. Komut paletini açın (Windows/Linux'ta Ctrl+Üst Karakter+P, macOS'te Cmd+Üst Karakter+P), "flutter new" yazın ve menüden Flutter: New Project'i seçin.

Flutter ile VS Code: Açık komut paletinde gösterilen yeni proje.

  1. Boş uygulama'yı seçin ve ardından projenizi oluşturacağınız bir dizin seçin. Bu, yükseltilmiş ayrıcalıklar gerektirmeyen veya yolunda boşluk olmayan herhangi bir dizin olmalıdır. Örneğin, ana dizininiz veya C:\src\.

Yeni uygulama akışının bir parçası olarak seçili gösterilen Boş Uygulama ile VS Code

  1. Projenizi adlandırın generate_crossword. Bu codelab'in geri kalanında uygulamanıza generate_crossword adını verdiğiniz varsayılır.

Oluşturulan yeni projenin adı olarak generate_crossword ile VS Code

Flutter artık proje klasörünüzü oluşturur ve VS Code bu klasörü açar. Şimdi iki dosyanın içeriğini uygulamanın temel iskeletiyle değiştireceksiniz.

İlk uygulamayı kopyalayıp yapıştırma

  1. VS Code'un sol bölmesinde Explorer'ı tıklayın ve pubspec.yaml dosyasını açın.

VS Code'un, pubspec.yaml dosyasının konumunu vurgulayan oklar içeren kısmi ekran görüntüsü

  1. Bu dosyanın içeriğini, bulmaca oluşturmak için gereken aşağıdaki bağımlılıklarla değiştirin:

pubspec.yaml

name: generate_crossword
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0

environment:
  sdk: ^3.9.0

dependencies:
  flutter:
    sdk: flutter
  built_collection: ^5.1.1
  built_value: ^8.10.1
  characters: ^1.4.0
  flutter_riverpod: ^2.6.1
  intl: ^0.20.2
  riverpod: ^2.6.1
  riverpod_annotation: ^2.6.1
  two_dimensional_scrollables: ^0.3.7

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0
  build_runner: ^2.5.4
  built_value_generator: ^8.10.1
  custom_lint: ^0.7.6
  riverpod_generator: ^2.6.5
  riverpod_lint: ^2.6.5

flutter:
  uses-material-design: true

pubspec.yaml dosyası, uygulamanızla ilgili temel bilgileri (ör. mevcut sürümü ve bağımlılıkları) belirtir. Normal boş bir Flutter uygulamasında bulunmayan bir dizi bağımlılık görürsünüz. Sonraki adımlarda bu paketlerin tümünden yararlanacaksınız.

Bağımlılıkları Anlama

Koda geçmeden önce neden bu paketlerin seçildiğini anlayalım:

  • built_value: Geri izleme algoritmamız için çok önemli olan, belleği verimli bir şekilde paylaşan değişmez nesneler oluşturur.
  • Riverpod: Yeniden oluşturma işlemlerini en aza indirmek için select() ile ayrıntılı durum yönetimi sağlar.
  • two_dimensional_scrollables: Performansta düşüş olmadan büyük ızgaraları işler
  1. main.dart dosyasını lib/ dizininde açın.

VS Code'un kısmi ekran görüntüsünde, main.dart dosyasının konumunu gösteren bir ok yer alıyor.

  1. Bu dosyanın içeriğini aşağıdakiyle değiştirin:

lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(
    ProviderScope(
      child: MaterialApp(
        title: 'Crossword Builder',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          colorSchemeSeed: Colors.blueGrey,
          brightness: Brightness.light,
        ),
        home: Scaffold(
          body: Center(
            child: Text('Hello, World!', style: TextStyle(fontSize: 24)),
          ),
        ),
      ),
    ),
  );
}
  1. Her şeyin çalıştığından emin olmak için bu kodu çalıştırın. Her yeni projenin zorunlu başlangıç ifadesini içeren yeni bir pencere gösterilmelidir. Bu uygulamanın durum yönetimi için riverpod kullanacağını belirten bir ProviderScope var.

Ortasında "Hello, World!" yazan bir uygulama penceresi

Kontrol noktası: Temel uygulama çalıştırma

Bu noktada "Hello, World!" penceresini görmeniz gerekir. Yoksa:

  • Flutter'ın düzgün şekilde yüklendiğinden emin olun.
  • Uygulamanın flutter run ile çalıştığını doğrulayın.
  • Terminalde derleme hatası olmadığından emin olun.

3. Kelime ekleme

Çapraz bulmaca için yapı taşları

Bulmaca, özünde bir kelime listesidir. Kelimeler, birbirine geçecek şekilde bazıları yatay, bazıları dikey olarak bir ızgara içinde düzenlenir. Bir kelimeyi çözmek, o kelimeyle kesişen kelimeler hakkında ipuçları verir. Bu nedenle, iyi bir ilk yapı taşı kelime listesidir.

Bu kelimeler için iyi bir kaynak, Peter Norvig'in Natural Language Corpus Data (Doğal Dil Korpus Verileri) sayfasıdır. 267.750 kelimeden oluşan SOWPODS listesi, yararlı bir başlangıç noktasıdır.

Bu adımda, bir kelime listesi indirir, bunu Flutter uygulamanıza öğe olarak ekler ve listeyi başlangıçta uygulamaya yüklemek için bir Riverpod sağlayıcısı düzenlersiniz.

Başlamak için aşağıdaki adımları uygulayın:

  1. Seçtiğiniz kelime listeniz için aşağıdaki öğe bildirimini eklemek üzere projenizin pubspec.yaml dosyasını değiştirin. Bu giriş, uygulamanızın yapılandırmasının yalnızca Flutter bölümünü gösterir. Diğer bölümler aynı kalmıştır.

pubspec.yaml

flutter:
  uses-material-design: true
  assets:                                       # Add this line
    - assets/words.txt                          # And this one.

Düzenleyiciniz, bu dosyayı henüz oluşturmadığınız için son satırı uyarı ile vurgulayacaktır.

  1. Tarayıcınızı ve düzenleyicinizi kullanarak projenizin en üst düzeyinde bir assets dizini oluşturun ve bu dizinde daha önce bağlantısı verilen kelime listelerinden birini içeren bir words.txt dosyası oluşturun.

Bu kod, daha önce bahsedilen SOWPODS listesiyle kullanılmak üzere tasarlanmıştır ancak yalnızca A-Z karakterlerinden oluşan herhangi bir kelime listesiyle de çalışır. Bu kod tabanını farklı karakter kümeleriyle çalışacak şekilde genişletmek okuyucunun sorumluluğundadır.

Kelimeleri yükleme

Uygulama başlatıldığında kelime listesini yüklemekten sorumlu kodu yazmak için aşağıdaki adımları uygulayın:

  1. lib dizininde bir providers.dart dosyası oluşturun.
  2. Dosyaya aşağıdakileri ekleyin:

lib/providers.dart

import 'dart:convert';

import 'package:built_collection/built_collection.dart';
import 'package:flutter/services.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'providers.g.dart';

/// A provider for the wordlist to use when generating the crossword.
@riverpod
Future<BuiltSet<String>> wordList(Ref ref) async {
  // This codebase requires that all words consist of lowercase characters
  // in the range 'a'-'z'. Words containing uppercase letters will be
  // lowercased, and words containing runes outside this range will
  // be removed.

  final re = RegExp(r'^[a-z]+$');
  final words = await rootBundle.loadString('assets/words.txt');
  return const LineSplitter()
      .convert(words)
      .toBuiltSet()
      .rebuild(
        (b) => b
          ..map((word) => word.toLowerCase().trim())
          ..where((word) => word.length > 2)
          ..where((word) => re.hasMatch(word)),
      );
}

Bu, kod tabanı için ilk Riverpod sağlayıcınızdır.

Bu sağlayıcının işleyiş şekli:

  1. Kelime listesini öğelerden eşzamansız olarak yükler.
  2. Yalnızca 2 harften uzun a-z karakterlerini içerecek şekilde kelimeleri filtreler.
  3. Verimli rastgele erişim için sabit bir BuiltSet döndürür.

Bu proje, Riverpod dahil olmak üzere birden fazla bağımlılık için kod oluşturma özelliğini kullanıyor.

  1. Kod oluşturmaya başlamak için aşağıdaki komutu çalıştırın:
$ dart run build_runner watch -d
[INFO] Generating build script completed, took 174ms
[INFO] Setting up file watchers completed, took 5ms
[INFO] Waiting for all file watchers to be ready completed, took 202ms
[INFO] Reading cached asset graph completed, took 65ms
[INFO] Checking for updates since last build completed, took 680ms
[INFO] Running build completed, took 2.3s
[INFO] Caching finalized dependency graph completed, took 42ms
[INFO] Succeeded after 2.3s with 122 outputs (243 actions)

Arka planda çalışmaya devam ederek projede değişiklik yaptıkça oluşturulan dosyaları günceller. Bu komut providers.g.dart içinde kodu oluşturduktan sonra düzenleyiciniz, providers.dart'ye eklediğiniz koddan memnun kalacaktır.

Riverpod'da, daha önce tanımladığınız wordList işlevi gibi sağlayıcılar genellikle geç başlatılır. Ancak bu uygulamanın amaçları doğrultusunda kelime listesinin hemen yüklenmesi gerekir. Riverpod dokümanlarında, hemen yüklenmesi gereken sağlayıcılarla ilgili olarak aşağıdaki yaklaşım önerilmektedir. Şimdi bunu uygulayacaksınız.

  1. crossword_generator_app.dart dizininde lib/widgets dosyası oluşturun.
  2. Dosyaya aşağıdakileri ekleyin:

lib/widgets/crossword_generator_app.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../providers.dart';

class CrosswordGeneratorApp extends StatelessWidget {
  const CrosswordGeneratorApp({super.key});

  @override
  Widget build(BuildContext context) {
    return _EagerInitialization(
      child: Scaffold(
        appBar: AppBar(
          titleTextStyle: TextStyle(
            color: Theme.of(context).colorScheme.primary,
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
          title: Text('Crossword Generator'),
        ),
        body: SafeArea(
          child: Consumer(
            builder: (context, ref, _) {
              final wordListAsync = ref.watch(wordListProvider);
              return wordListAsync.when(
                data: (wordList) => ListView.builder(
                  itemCount: wordList.length,
                  itemBuilder: (context, index) {
                    return ListTile(title: Text(wordList.elementAt(index)));
                  },
                ),
                error: (error, stackTrace) => Center(child: Text('$error')),
                loading: () => Center(child: CircularProgressIndicator()),
              );
            },
          ),
        ),
      ),
    );
  }
}

class _EagerInitialization extends ConsumerWidget {
  const _EagerInitialization({required this.child});
  final Widget child;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.watch(wordListProvider);
    return child;
  }
}

Bu dosya iki ayrı yönden ilgi çekicidir. Birincisi, daha önce oluşturduğunuz wordList sağlayıcının kelime listesini yüklemesini gerektirmekten başka bir görevi olmayan _EagerInitialization widget'ıdır. Bu widget, ref.watch() çağrısını kullanarak sağlayıcıyı dinleyerek bu amaca ulaşır. Bu teknik hakkında daha fazla bilgiyi Eager initialization of providers (Sağlayıcıların istekli başlatılması) başlıklı Riverpod dokümanında bulabilirsiniz.

Bu dosyada dikkat çekilmesi gereken ikinci nokta, Riverpod'un asenkron içeriği nasıl işlediğidir. Hatırlayacağınız gibi, diskten içerik yüklemek yavaş olduğundan wordList sağlayıcısı eşzamansız bir işlev olarak tanımlanır. Bu kodda kelime listesi sağlayıcısını izlediğinizde AsyncValue<BuiltSet<String>> alırsınız. Bu türün AsyncValue kısmı, sağlayıcıların eşzamansız dünyası ile widget'ın build yönteminin eşzamanlı dünyası arasında bir bağdaştırıcıdır.

AsyncValue'nın when yöntemi, gelecekteki değerin bulunabileceği üç olası durumu ele alır. Gelecek başarılı bir şekilde çözülmüş olabilir. Bu durumda data geri çağırma işlemi çağrılır. Gelecek, hata durumunda olabilir. Bu durumda error geri çağırma işlemi çağrılır. Son olarak, gelecek hâlâ yükleniyor olabilir. Çağrılan geri çağırma işlevinin dönüşü when yöntemi tarafından döndürüldüğünden, üç geri çağırma işlevinin dönüş türleri uyumlu olmalıdır. Bu örnekte, when yönteminin sonucu Scaffold widget'ının body olarak gösterilir.

Neredeyse sonsuz bir liste uygulaması oluşturma

CrosswordGeneratorApp widget'ını uygulamanıza entegre etmek için aşağıdaki adımları uygulayın:

  1. Aşağıdaki kodu ekleyerek lib/main.dart dosyasını güncelleyin:

lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'widgets/crossword_generator_app.dart';             // Add this import

void main() {
  runApp(
    ProviderScope(
      child: MaterialApp(
        title: 'Crossword Builder',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          colorSchemeSeed: Colors.blueGrey,
          brightness: Brightness.light,
        ),
        home: CrosswordGeneratorApp(),                     // Remove what was here and replace
      ),
    ),
  );
}
  1. Uygulamayı yeniden başlatın. Sözlükteki 267.750'den fazla kelimenin tamamını içeren kayan bir liste görürsünüz.

&quot;Bulmaca Oluşturucu&quot; başlıklı ve kelime listesi içeren bir uygulama penceresi

Bir sonraki adımda ne oluşturacaksınız?

Şimdi, değişmez nesneleri kullanarak bulmacanızın temel veri yapılarını oluşturacaksınız. Bu temel, verimli algoritmalar ve sorunsuz kullanıcı arayüzü güncellemeleri sağlar.

4. Kelimeleri ızgara şeklinde gösterme

Bu adımda, built_value ve built_collection paketlerini kullanarak bulmaca oluşturmak için bir veri yapısı oluşturacaksınız. Bu iki paket, veri yapılarını değişmez değerler olarak oluşturmayı sağlar. Bu, hem Isolates arasında veri aktarmak hem de derinlemesine arama ve geri izlemeyi çok daha kolay uygulamak için yararlı olacaktır.

Başlamak için aşağıdaki adımları uygulayın:

  1. lib dizininde bir model.dart dosyası oluşturun ve dosyaya aşağıdaki içeriği ekleyin:

lib/model.dart

import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:characters/characters.dart';

part 'model.g.dart';

/// A location in a crossword.
abstract class Location implements Built<Location, LocationBuilder> {
  static Serializer<Location> get serializer => _$locationSerializer;

  /// The horizontal part of the location. The location is 0 based.
  int get x;

  /// The vertical part of the location. The location is 0 based.
  int get y;

  /// Returns a new location that is one step to the left of this location.
  Location get left => rebuild((b) => b.x = x - 1);

  /// Returns a new location that is one step to the right of this location.
  Location get right => rebuild((b) => b.x = x + 1);

  /// Returns a new location that is one step up from this location.
  Location get up => rebuild((b) => b.y = y - 1);

  /// Returns a new location that is one step down from this location.
  Location get down => rebuild((b) => b.y = y + 1);

  /// Returns a new location that is [offset] steps to the left of this location.
  Location leftOffset(int offset) => rebuild((b) => b.x = x - offset);

  /// Returns a new location that is [offset] steps to the right of this location.
  Location rightOffset(int offset) => rebuild((b) => b.x = x + offset);

  /// Returns a new location that is [offset] steps up from this location.
  Location upOffset(int offset) => rebuild((b) => b.y = y - offset);

  /// Returns a new location that is [offset] steps down from this location.
  Location downOffset(int offset) => rebuild((b) => b.y = y + offset);

  /// Pretty print a location as a (x,y) coordinate.
  String prettyPrint() => '($x,$y)';

  /// Returns a new location built from [updates]. Both [x] and [y] are
  /// required to be non-null.
  factory Location([void Function(LocationBuilder)? updates]) = _$Location;
  Location._();

  /// Returns a location at the given coordinates.
  factory Location.at(int x, int y) {
    return Location((b) {
      b
        ..x = x
        ..y = y;
    });
  }
}

/// The direction of a word in a crossword.
enum Direction {
  across,
  down;

  @override
  String toString() => name;
}

/// A word in a crossword. This is a word at a location in a crossword, in either
/// the across or down direction.
abstract class CrosswordWord
    implements Built<CrosswordWord, CrosswordWordBuilder> {
  static Serializer<CrosswordWord> get serializer => _$crosswordWordSerializer;

  /// The word itself.
  String get word;

  /// The location of this word in the crossword.
  Location get location;

  /// The direction of this word in the crossword.
  Direction get direction;

  /// Compare two CrosswordWord by coordinates, x then y.
  static int locationComparator(CrosswordWord a, CrosswordWord b) {
    final compareRows = a.location.y.compareTo(b.location.y);
    final compareColumns = a.location.x.compareTo(b.location.x);
    return switch (compareColumns) {
      0 => compareRows,
      _ => compareColumns,
    };
  }

  /// Constructor for [CrosswordWord].
  factory CrosswordWord.word({
    required String word,
    required Location location,
    required Direction direction,
  }) {
    return CrosswordWord(
      (b) => b
        ..word = word
        ..direction = direction
        ..location.replace(location),
    );
  }

  /// Constructor for [CrosswordWord].
  /// Use [CrosswordWord.word] instead.
  factory CrosswordWord([void Function(CrosswordWordBuilder)? updates]) =
      _$CrosswordWord;
  CrosswordWord._();
}

/// A character in a crossword. This is a single character at a location in a
/// crossword. It may be part of an across word, a down word, both, but not
/// neither. The neither constraint is enforced elsewhere.
abstract class CrosswordCharacter
    implements Built<CrosswordCharacter, CrosswordCharacterBuilder> {
  static Serializer<CrosswordCharacter> get serializer =>
      _$crosswordCharacterSerializer;

  /// The character at this location.
  String get character;

  /// The across word that this character is a part of.
  CrosswordWord? get acrossWord;

  /// The down word that this character is a part of.
  CrosswordWord? get downWord;

  /// Constructor for [CrosswordCharacter].
  /// [acrossWord] and [downWord] are optional.
  factory CrosswordCharacter.character({
    required String character,
    CrosswordWord? acrossWord,
    CrosswordWord? downWord,
  }) {
    return CrosswordCharacter((b) {
      b.character = character;
      if (acrossWord != null) {
        b.acrossWord.replace(acrossWord);
      }
      if (downWord != null) {
        b.downWord.replace(downWord);
      }
    });
  }

  /// Constructor for [CrosswordCharacter].
  /// Use [CrosswordCharacter.character] instead.
  factory CrosswordCharacter([
    void Function(CrosswordCharacterBuilder)? updates,
  ]) = _$CrosswordCharacter;
  CrosswordCharacter._();
}

/// A crossword puzzle. This is a grid of characters with words placed in it.
/// The puzzle constraint is in the English crossword puzzle tradition.
abstract class Crossword implements Built<Crossword, CrosswordBuilder> {
  /// Serializes and deserializes the [Crossword] class.
  static Serializer<Crossword> get serializer => _$crosswordSerializer;

  /// Width across the [Crossword] puzzle.
  int get width;

  /// Height down the [Crossword] puzzle.
  int get height;

  /// The words in the crossword.
  BuiltList<CrosswordWord> get words;

  /// The characters by location. Useful for displaying the crossword.
  BuiltMap<Location, CrosswordCharacter> get characters;

  /// Add a word to the crossword at the given location and direction.
  Crossword addWord({
    required Location location,
    required String word,
    required Direction direction,
  }) {
    return rebuild(
      (b) => b
        ..words.add(
          CrosswordWord.word(
            word: word,
            direction: direction,
            location: location,
          ),
        ),
    );
  }

  /// As a finalize step, fill in the characters map.
  @BuiltValueHook(finalizeBuilder: true)
  static void _fillCharacters(CrosswordBuilder b) {
    b.characters.clear();

    for (final word in b.words.build()) {
      for (final (idx, character) in word.word.characters.indexed) {
        switch (word.direction) {
          case Direction.across:
            b.characters.updateValue(
              word.location.rightOffset(idx),
              (b) => b.rebuild((bInner) => bInner.acrossWord.replace(word)),
              ifAbsent: () => CrosswordCharacter.character(
                acrossWord: word,
                character: character,
              ),
            );
          case Direction.down:
            b.characters.updateValue(
              word.location.downOffset(idx),
              (b) => b.rebuild((bInner) => bInner.downWord.replace(word)),
              ifAbsent: () => CrosswordCharacter.character(
                downWord: word,
                character: character,
              ),
            );
        }
      }
    }
  }

  /// Pretty print a crossword. Generates the character grid, and lists
  /// the down words and across words sorted by location.
  String prettyPrintCrossword() {
    final buffer = StringBuffer();
    final grid = List.generate(
      height,
      (_) => List.generate(
        width,
        (_) => '░', // https://www.compart.com/en/unicode/U+2591
      ),
    );

    for (final MapEntry(key: Location(:x, :y), value: character)
        in characters.entries) {
      grid[y][x] = character.character;
    }

    for (final row in grid) {
      buffer.writeln(row.join());
    }

    buffer.writeln();
    buffer.writeln('Across:');
    for (final word
        in words.where((word) => word.direction == Direction.across).toList()
          ..sort(CrosswordWord.locationComparator)) {
      buffer.writeln('${word.location.prettyPrint()}: ${word.word}');
    }

    buffer.writeln();
    buffer.writeln('Down:');
    for (final word
        in words.where((word) => word.direction == Direction.down).toList()
          ..sort(CrosswordWord.locationComparator)) {
      buffer.writeln('${word.location.prettyPrint()}: ${word.word}');
    }

    return buffer.toString();
  }

  /// Constructor for [Crossword].
  factory Crossword.crossword({
    required int width,
    required int height,
    Iterable<CrosswordWord>? words,
  }) {
    return Crossword((b) {
      b
        ..width = width
        ..height = height;
      if (words != null) {
        b.words.addAll(words);
      }
    });
  }

  /// Constructor for [Crossword].
  /// Use [Crossword.crossword] instead.
  factory Crossword([void Function(CrosswordBuilder)? updates]) = _$Crossword;
  Crossword._();
}

/// Construct the serialization/deserialization code for the data model.
@SerializersFor([Location, Crossword, CrosswordWord, CrosswordCharacter])
final Serializers serializers = _$serializers;

Bu dosya, bulmaca oluşturmak için kullanacağınız veri yapısının başlangıcını açıklar. Çengel bulmaca, temelde bir tabloya yerleştirilmiş yatay ve dikey kelimelerden oluşan bir listedir. Bu veri yapısını kullanmak için Crossword.crossword adlı oluşturucuyla uygun boyutta bir Crossword oluşturur, ardından addWord yöntemini kullanarak kelimeler eklersiniz. Sonlandırılmış değer oluşturulurken CrosswordCharacter yöntemiyle bir _fillCharacters ızgarası oluşturulur.

Bu veri yapısını kullanmak için aşağıdaki adımları uygulayın:

  1. lib dizininde bir utils dosyası oluşturun ve dosyaya aşağıdaki içeriği ekleyin:

lib/utils.dart

import 'dart:math';

import 'package:built_collection/built_collection.dart';

/// A [Random] instance for generating random numbers.
final _random = Random();

/// An extension on [BuiltSet] that adds a method to get a random element.
extension RandomElements<E> on BuiltSet<E> {
  E randomElement() {
    return elementAt(_random.nextInt(length));
  }
}

Bu, BuiltSet üzerinde, kümenin rastgele bir öğesini kolayca almayı sağlayan bir uzantıdır. Uzantı yöntemleri, sınıfları ek işlevlerle genişletmek için iyi bir yoldur. Uzantının utils.dart dosyası dışında kullanılabilmesi için adlandırılması gerekir.

  1. lib/providers.dart dosyanıza aşağıdaki içe aktarma işlemlerini ekleyin:

lib/providers.dart

import 'dart:convert';
import 'dart:math';                                        // Add this import

import 'package:built_collection/built_collection.dart';
import 'package:flutter/foundation.dart';                  // Add this import
import 'package:flutter/services.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'model.dart' as model;                              // And this import
import 'utils.dart';                                       // And this one

part 'providers.g.dart';

/// A provider for the wordlist to use when generating the crossword.
@riverpod
Future<BuiltSet<String>> wordList(WordListRef ref) async {

Bu içe aktarma işlemleri, daha önce tanımlanmış modeli oluşturmak üzere olduğunuz sağlayıcılara sunar. dart:math içe aktarma işlemi Random için, flutter/foundation.dart içe aktarma işlemi debugPrint için, model.dart model için ve utils.dart BuiltSet uzantısı için dahil edilmiştir.

  1. Aynı dosyanın sonuna aşağıdaki sağlayıcıları ekleyin:

lib/providers.dart

/// An enumeration for different sizes of [model.Crossword]s.
enum CrosswordSize {
  small(width: 20, height: 11),
  medium(width: 40, height: 22),
  large(width: 80, height: 44),
  xlarge(width: 160, height: 88),
  xxlarge(width: 500, height: 500);

  const CrosswordSize({required this.width, required this.height});

  final int width;
  final int height;
  String get label => '$width x $height';
}

/// A provider that holds the current size of the crossword to generate.
@Riverpod(keepAlive: true)
class Size extends _$Size {
  var _size = CrosswordSize.medium;

  @override
  CrosswordSize build() => _size;

  void setSize(CrosswordSize size) {
    _size = size;
    ref.invalidateSelf();
  }
}

final _random = Random();

@riverpod
Stream<model.Crossword> crossword(Ref ref) async* {
  final size = ref.watch(sizeProvider);
  final wordListAsync = ref.watch(wordListProvider);

  var crossword = model.Crossword.crossword(
    width: size.width,
    height: size.height,
  );

  yield* wordListAsync.when(
    data: (wordList) async* {
      while (crossword.characters.length < size.width * size.height * 0.8) {
        final word = wordList.randomElement();
        final direction = _random.nextBool()
            ? model.Direction.across
            : model.Direction.down;
        final location = model.Location.at(
          _random.nextInt(size.width),
          _random.nextInt(size.height),
        );

        crossword = crossword.addWord(
          word: word,
          direction: direction,
          location: location,
        );
        yield crossword;
        await Future.delayed(Duration(milliseconds: 100));
      }

      yield crossword;
    },
    error: (error, stackTrace) async* {
      debugPrint('Error loading word list: $error');
      yield crossword;
    },
    loading: () async* {
      yield crossword;
    },
  );
}

Bu değişiklikler, uygulamanıza iki sağlayıcı ekler. Bunlardan ilki, Size. Bu, CrosswordSize numaralandırmasının seçilen değerini içeren global bir değişkendir. Bu, kullanıcı arayüzünün yapım aşamasındaki bulmacanın boyutunu hem görüntülemesine hem de ayarlamasına olanak tanır. İkinci sağlayıcı olan crossword daha ilginç bir yapımdır. Bir dizi Crossword döndüren bir işlevdir. İşlevdeki async* ile işaretlendiği gibi, Dart'ın oluşturucu desteği kullanılarak oluşturulur. Bu, bir satır sonuyla bitmek yerine bir dizi Crossword döndürdüğü anlamına gelir. Bu, ara sonuçlar döndüren bir hesaplama yazmanın çok daha kolay bir yoludur.

crossword sağlayıcı işlevinin başında bir çift ref.watch çağrısı bulunduğundan, bulmacanın seçilen boyutu her değiştiğinde ve kelime listesi yüklenmeyi tamamladığında Crossword akışı Riverpod sistemi tarafından yeniden başlatılır.

Artık rastgele kelimelerle dolu olsa da bulmaca oluşturmak için kodunuz olduğuna göre, bunları aracın kullanıcısına göstermek iyi bir fikir olabilir.

  1. lib/widgets dizininde aşağıdaki içeriğe sahip bir crossword_widget.dart dosyası oluşturun:

lib/widgets/crossword_widget.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart';

import '../model.dart';
import '../providers.dart';

class CrosswordWidget extends ConsumerWidget {
  const CrosswordWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final size = ref.watch(sizeProvider);
    return TableView.builder(
      diagonalDragBehavior: DiagonalDragBehavior.free,
      cellBuilder: _buildCell,
      columnCount: size.width,
      columnBuilder: (index) => _buildSpan(context, index),
      rowCount: size.height,
      rowBuilder: (index) => _buildSpan(context, index),
    );
  }

  TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) {
    final location = Location.at(vicinity.column, vicinity.row);

    return TableViewCell(
      child: Consumer(
        builder: (context, ref, _) {
          final character = ref.watch(
            crosswordProvider.select(
              (crosswordAsync) => crosswordAsync.when(
                data: (crossword) => crossword.characters[location],
                error: (error, stackTrace) => null,
                loading: () => null,
              ),
            ),
          );

          if (character != null) {
            return Container(
              color: Theme.of(context).colorScheme.onPrimary,
              child: Center(
                child: Text(
                  character.character,
                  style: TextStyle(
                    fontSize: 24,
                    color: Theme.of(context).colorScheme.primary,
                  ),
                ),
              ),
            );
          }

          return ColoredBox(
            color: Theme.of(context).colorScheme.primaryContainer,
          );
        },
      ),
    );
  }

  TableSpan _buildSpan(BuildContext context, int index) {
    return TableSpan(
      extent: FixedTableSpanExtent(32),
      foregroundDecoration: TableSpanDecoration(
        border: TableSpanBorder(
          leading: BorderSide(
            color: Theme.of(context).colorScheme.onPrimaryContainer,
          ),
          trailing: BorderSide(
            color: Theme.of(context).colorScheme.onPrimaryContainer,
          ),
        ),
      ),
    );
  }
}

Bu widget, ConsumerWidget olduğundan Crossword karakterlerini göstereceği ızgaranın boyutunu belirlemek için doğrudan Size sağlayıcısına güvenebilir. Bu ızgaranın gösterimi, two_dimensional_scrollables paketindeki TableView widget'ı ile gerçekleştirilir.

_buildCell yardımcı işlevleri tarafından oluşturulan hücrelerin her birinin, döndürülen Widget ağacında bir Consumer widget'ı içerdiğini belirtmekte fayda var. Bu, yenileme sınırı görevi görür. Consumer widget'ının içindeki her şey, ref.watch'nin döndürülen değeri değiştiğinde yeniden oluşturulur. Crossword her değiştiğinde ağacın tamamını yeniden oluşturmak cazip gelebilir ancak bu kurulum kullanılarak atlanabilecek çok fazla hesaplama yapılmasına neden olur.

ref.watch parametresine bakarsanız crosswordProvider.select kullanılarak düzenlerin yeniden hesaplanmasının önlenmesi için başka bir katman olduğunu görürsünüz. Bu, ref.watch işlevinin, yalnızca hücrenin oluşturmaktan sorumlu olduğu karakter değiştiğinde TableViewCell içeriğinin yeniden oluşturulmasını tetikleyeceği anlamına gelir. Yeniden oluşturma sayısındaki bu azalma, kullanıcı arayüzünün yanıt vermesini sağlamanın önemli bir parçasıdır.

CrosswordWidget ve Size sağlayıcıyı kullanıcıya göstermek için crossword_generator_app.dart dosyasını aşağıdaki gibi değiştirin:

lib/widgets/crossword_generator_app.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../providers.dart';
import 'crossword_widget.dart';                               // Add this import

class CrosswordGeneratorApp extends StatelessWidget {
  const CrosswordGeneratorApp({super.key});

  @override
  Widget build(BuildContext context) {
    return _EagerInitialization(
      child: Scaffold(
        appBar: AppBar(
          actions: [_CrosswordGeneratorMenu()],               // Add this line
          titleTextStyle: TextStyle(
            color: Theme.of(context).colorScheme.primary,
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
          title: Text('Crossword Generator'),
        ),
        body: SafeArea(child: CrosswordWidget()),             // Replace what was here before
      ),
    );
  }
}

class _EagerInitialization extends ConsumerWidget {
  const _EagerInitialization({required this.child});
  final Widget child;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.watch(wordListProvider);
    return child;
  }
}

class _CrosswordGeneratorMenu extends ConsumerWidget {        // Add from here
  @override
  Widget build(BuildContext context, WidgetRef ref) => MenuAnchor(
    menuChildren: [
      for (final entry in CrosswordSize.values)
        MenuItemButton(
          onPressed: () => ref.read(sizeProvider.notifier).setSize(entry),
          leadingIcon: entry == ref.watch(sizeProvider)
              ? Icon(Icons.radio_button_checked_outlined)
              : Icon(Icons.radio_button_unchecked_outlined),
          child: Text(entry.label),
        ),
    ],
    builder: (context, controller, child) => IconButton(
      onPressed: () => controller.open(),
      icon: Icon(Icons.settings),
    ),
  );                                                          // To here.
}

Burada birkaç şey değişti. İlk olarak, wordList öğesini ListView olarak oluşturmaktan sorumlu kod, lib/widgets/crossword_widget.dart dosyasında tanımlanan CrosswordWidget öğesine yapılan bir çağrıyla değiştirildi. Diğer önemli değişiklik ise uygulamanın davranışını değiştirmeye yönelik bir menünün kullanıma sunulmasıdır. Bu menüde ilk olarak bulmaca boyutunu değiştirebilirsiniz. Gelecekteki adımlarda daha fazla MenuItemButton eklenecektir. Uygulamanızı çalıştırdığınızda aşağıdakine benzer bir şey görürsünüz:

Başlığı &quot;Bulmaca Oluşturucu&quot; olan bir uygulama penceresi ve kafiyeli veya mantıklı olmayan, çakışan kelimeler şeklinde düzenlenmiş karakterlerden oluşan bir ızgara

Izgarada gösterilen karakterler ve kullanıcının ızgaranın boyutunu değiştirmesine olanak tanıyan bir menü vardır. Ancak kelimeler, bulmacadaki gibi yerleştirilmez. Bu durum, kelimelerin bulmacaya eklenme şekliyle ilgili herhangi bir kısıtlama uygulanmamasından kaynaklanır. Kısacası, her şey karışık. Sonraki adımda kontrol altına almaya başlayacağınız bir şey!

5. Kısıtlamaları zorunlu kılma

Neleri değiştiriyoruz ve neden?

Bulmacanız şu anda doğrulanmadan kelimelerin çakışmasına izin veriyor. Kelimelerin gerçek bir bulmacadaki gibi birbirine doğru şekilde bağlanmasını sağlamak için kısıtlama kontrolü ekleyeceksiniz.

Bu adımın amacı, bulmaca kısıtlamalarını zorunlu kılmak için modele kod eklemektir. Birçok farklı türde bulmaca vardır ve bu codelab'de kullanılacak stil, İngilizce bulmacaların geleneklerine uygundur. Bu kodu, başka tarzda bulmacalar oluşturacak şekilde değiştirmek her zamanki gibi okuyucunun sorumluluğundadır.

Başlamak için aşağıdaki adımları uygulayın:

  1. model.dart dosyasını açın ve yalnızca Crossword modelini aşağıdakilerle değiştirin:

lib/model.dart

/// A crossword puzzle. This is a grid of characters with words placed in it.
/// The puzzle constraint is in the English crossword puzzle tradition.
abstract class Crossword implements Built<Crossword, CrosswordBuilder> {
  /// Serializes and deserializes the [Crossword] class.
  static Serializer<Crossword> get serializer => _$crosswordSerializer;

  /// Width across the [Crossword] puzzle.
  int get width;

  /// Height down the [Crossword] puzzle.
  int get height;

  /// The words in the crossword.
  BuiltList<CrosswordWord> get words;

  /// The characters by location. Useful for displaying the crossword,
  /// or checking the proposed solution.
  BuiltMap<Location, CrosswordCharacter> get characters;

  /// Checks if this crossword is valid.
  bool get valid {
    // Check that there are no duplicate words.
    final wordSet = words.map((word) => word.word).toBuiltSet();
    if (wordSet.length != words.length) {
      return false;
    }

    for (final MapEntry(key: location, value: character)
        in characters.entries) {
      // All characters must be a part of an across or down word.
      if (character.acrossWord == null && character.downWord == null) {
        return false;
      }

      // All characters must be within the crossword puzzle.
      // No drawing outside the lines.
      if (location.x < 0 ||
          location.y < 0 ||
          location.x >= width ||
          location.y >= height) {
        return false;
      }

      // Characters above and below this character must be related
      // by a vertical word
      if (characters[location.up] case final up?) {
        if (character.downWord == null) {
          return false;
        }
        if (up.downWord != character.downWord) {
          return false;
        }
      }

      if (characters[location.down] case final down?) {
        if (character.downWord == null) {
          return false;
        }
        if (down.downWord != character.downWord) {
          return false;
        }
      }

      // Characters to the left and right of this character must be
      // related by a horizontal word
      final left = characters[location.left];
      if (left != null) {
        if (character.acrossWord == null) {
          return false;
        }
        if (left.acrossWord != character.acrossWord) {
          return false;
        }
      }

      final right = characters[location.right];
      if (right != null) {
        if (character.acrossWord == null) {
          return false;
        }
        if (right.acrossWord != character.acrossWord) {
          return false;
        }
      }
    }

    return true;
  }

  /// Add a word to the crossword at the given location and direction.
  Crossword? addWord({
    required Location location,
    required String word,
    required Direction direction,
  }) {
    // Require that the word is not already in the crossword.
    if (words.map((crosswordWord) => crosswordWord.word).contains(word)) {
      return null;
    }

    final wordCharacters = word.characters;
    bool overlap = false;

    // Check that the word fits in the crossword.
    for (final (index, character) in wordCharacters.indexed) {
      final characterLocation = switch (direction) {
        Direction.across => location.rightOffset(index),
        Direction.down => location.downOffset(index),
      };

      final target = characters[characterLocation];
      if (target != null) {
        overlap = true;
        if (target.character != character) {
          return null;
        }
        if (direction == Direction.across && target.acrossWord != null ||
            direction == Direction.down && target.downWord != null) {
          return null;
        }
      }
    }
    if (words.isNotEmpty && !overlap) {
      return null;
    }

    final candidate = rebuild(
      (b) => b
        ..words.add(
          CrosswordWord.word(
            word: word,
            direction: direction,
            location: location,
          ),
        ),
    );

    if (candidate.valid) {
      return candidate;
    } else {
      return null;
    }
  }

  /// As a finalize step, fill in the characters map.
  @BuiltValueHook(finalizeBuilder: true)
  static void _fillCharacters(CrosswordBuilder b) {
    b.characters.clear();

    for (final word in b.words.build()) {
      for (final (idx, character) in word.word.characters.indexed) {
        switch (word.direction) {
          case Direction.across:
            b.characters.updateValue(
              word.location.rightOffset(idx),
              (b) => b.rebuild((bInner) => bInner.acrossWord.replace(word)),
              ifAbsent: () => CrosswordCharacter.character(
                acrossWord: word,
                character: character,
              ),
            );
          case Direction.down:
            b.characters.updateValue(
              word.location.downOffset(idx),
              (b) => b.rebuild((bInner) => bInner.downWord.replace(word)),
              ifAbsent: () => CrosswordCharacter.character(
                downWord: word,
                character: character,
              ),
            );
        }
      }
    }
  }

  /// Pretty print a crossword. Generates the character grid, and lists
  /// the down words and across words sorted by location.
  String prettyPrintCrossword() {
    final buffer = StringBuffer();
    final grid = List.generate(
      height,
      (_) => List.generate(
        width,
        (_) => '░', // https://www.compart.com/en/unicode/U+2591
      ),
    );

    for (final MapEntry(key: Location(:x, :y), value: character)
        in characters.entries) {
      grid[y][x] = character.character;
    }

    for (final row in grid) {
      buffer.writeln(row.join());
    }

    buffer.writeln();
    buffer.writeln('Across:');
    for (final word
        in words.where((word) => word.direction == Direction.across).toList()
          ..sort(CrosswordWord.locationComparator)) {
      buffer.writeln('${word.location.prettyPrint()}: ${word.word}');
    }

    buffer.writeln();
    buffer.writeln('Down:');
    for (final word
        in words.where((word) => word.direction == Direction.down).toList()
          ..sort(CrosswordWord.locationComparator)) {
      buffer.writeln('${word.location.prettyPrint()}: ${word.word}');
    }

    return buffer.toString();
  }

  /// Constructor for [Crossword].
  factory Crossword.crossword({
    required int width,
    required int height,
    Iterable<CrosswordWord>? words,
  }) {
    return Crossword((b) {
      b
        ..width = width
        ..height = height;
      if (words != null) {
        b.words.addAll(words);
      }
    });
  }

  /// Constructor for [Crossword].
  /// Use [Crossword.crossword] instead.
  factory Crossword([void Function(CrosswordBuilder)? updates]) = _$Crossword;
  Crossword._();
}

Kısa bir hatırlatma olarak, model.dart ve providers.dart dosyalarında yaptığınız değişikliklerin ilgili model.g.dart ve providers.g.dart dosyalarını güncellemek için build_runner'nin çalıştırılmasını gerektirdiğini hatırlatırız. Bu dosyalar otomatik olarak güncellenmediyse build_runner ile dart run build_runner watch -d'yi tekrar başlatmanın tam zamanı.

Model katmanındaki bu yeni özellikten yararlanmak için sağlayıcı katmanını eşleşecek şekilde güncellemeniz gerekir.

  1. providers.dart dosyanızı aşağıdaki gibi düzenleyin:

lib/providers.dart

import 'dart:convert';
import 'dart:math';

import 'package:built_collection/built_collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'model.dart' as model;
import 'utils.dart';

part 'providers.g.dart';

/// A provider for the wordlist to use when generating the crossword.
@riverpod
Future<BuiltSet<String>> wordList(WordListRef ref) async {
  // This codebase requires that all words consist of lowercase characters
  // in the range 'a'-'z'. Words containing uppercase letters will be
  // lowercased, and words containing runes outside this range will
  // be removed.

  final re = RegExp(r'^[a-z]+$');
  final words = await rootBundle.loadString('assets/words.txt');
  return const LineSplitter().convert(words).toBuiltSet().rebuild((b) => b
    ..map((word) => word.toLowerCase().trim())
    ..where((word) => word.length > 2)
    ..where((word) => re.hasMatch(word)));
}

/// An enumeration for different sizes of [model.Crossword]s.
enum CrosswordSize {
  small(width: 20, height: 11),
  medium(width: 40, height: 22),
  large(width: 80, height: 44),
  xlarge(width: 160, height: 88),
  xxlarge(width: 500, height: 500);

  const CrosswordSize({
    required this.width,
    required this.height,
  });

  final int width;
  final int height;
  String get label => '$width x $height';
}

/// A provider that holds the current size of the crossword to generate.
@Riverpod(keepAlive: true)
class Size extends _$Size {
  var _size = CrosswordSize.medium;

  @override
  CrosswordSize build() => _size;

  void setSize(CrosswordSize size) {
    _size = size;
    ref.invalidateSelf();
  }
}

final _random = Random();

@riverpod
Stream<model.Crossword> crossword(CrosswordRef ref) async* {
  final size = ref.watch(sizeProvider);
  final wordListAsync = ref.watch(wordListProvider);

  var crossword =
      model.Crossword.crossword(width: size.width, height: size.height);

  yield* wordListAsync.when(
    data: (wordList) async* {
      while (crossword.characters.length < size.width * size.height * 0.8) {
        final word = wordList.randomElement();
        final direction =
            _random.nextBool() ? model.Direction.across : model.Direction.down;
        final location = model.Location.at(
            _random.nextInt(size.width), _random.nextInt(size.height));

        var candidate = crossword.addWord(                 // Edit from here
            word: word, direction: direction, location: location);
        await Future.delayed(Duration(milliseconds: 10));
        if (candidate != null) {
          debugPrint('Added word: $word');
          crossword = candidate;
          yield crossword;
        } else {
          debugPrint('Failed to add word: $word');
        }                                                  // To here.
      }

      yield crossword;
    },
    error: (error, stackTrace) async* {
      debugPrint('Error loading word list: $error');
      yield crossword;
    },
    loading: () async* {
      yield crossword;
    },
  );
}
  1. Uygulamanızı çalıştırın. Kullanıcı arayüzünde pek bir şey olmuyor ancak günlük kayıtlarına baktığınızda çok şey olduğunu göreceksiniz.

Kelimelerin yatay ve dikey olarak yerleştirildiği, rastgele noktalarda kesiştiği Çapraz Bulmaca Oluşturucu uygulama penceresi

Burada ne olduğunu düşünürseniz rastgele bir bulmaca göründüğünü görürsünüz. Crossword modelindeki addWord yöntemi, mevcut bulmacaya uymayan tüm önerilen kelimeleri reddediyor. Bu nedenle, herhangi bir kelimenin görünmesi bile şaşırtıcı.

Neden arka planda işlemeye geçmelisiniz?

Bulmaca oluşturma sırasında kullanıcı arayüzünün yanıt vermediğini fark edebilirsiniz. Bunun nedeni, bulmaca oluşturma sürecinde binlerce doğrulama kontrolü yapılmasıdır. Bu hesaplamalar, Flutter'ın 60 FPS'lik oluşturma döngüsünü engeller. Bu nedenle, yoğun hesaplamaları arka plan yalıtımlarına taşırsınız. Bu sayede, arka planda bulmaca oluşturulurken kullanıcı arayüzü sorunsuz çalışmaya devam eder.

Hangi kelimeleri nerede deneyeceğinizi seçme konusunda daha metodik olmaya hazırlanırken bu hesaplamayı kullanıcı arayüzü iş parçacığından çıkarıp arka planda yalıtılmış bir alana taşımanız çok faydalı olacaktır. Flutter'da, bir iş parçasını alıp arka planda izole bir şekilde çalıştırmak için çok kullanışlı bir sarmalayıcı bulunur: compute işlevi.

  1. providers.dart dosyasında, bulmaca sağlayıcıyı aşağıdaki şekilde değiştirin:

lib/providers.dart

@riverpod
Stream<model.Crossword> crossword(Ref ref) async* {
  final size = ref.watch(sizeProvider);
  final wordListAsync = ref.watch(wordListProvider);

  var crossword = model.Crossword.crossword(
    width: size.width,
    height: size.height,
  );

  yield* wordListAsync.when(
    data: (wordList) async* {
      while (crossword.characters.length < size.width * size.height * 0.8) {
        final word = wordList.randomElement();
        final direction = _random.nextBool()
            ? model.Direction.across
            : model.Direction.down;
        final location = model.Location.at(
          _random.nextInt(size.width),
          _random.nextInt(size.height),
        );
        try {                                              // Edit from here
          var candidate = await compute((
            (String, model.Direction, model.Location) wordToAdd,
          ) {
            final (word, direction, location) = wordToAdd;
            return crossword.addWord(
              word: word,
              direction: direction,
              location: location,
            );
          }, (word, direction, location));

          if (candidate != null) {
            crossword = candidate;
            yield crossword;
          }
        } catch (e) {
          debugPrint('Error running isolate: $e');
        }                                                  // To here.
      }

      yield crossword;
    },
    error: (error, stackTrace) async* {
      debugPrint('Error loading word list: $error');
      yield crossword;
    },
    loading: () async* {
      yield crossword;
    },
  );
}

İzolasyon Kısıtlamalarını Anlama

Bu kod çalışıyor ancak gizli bir sorun var. İzoleler arasında hangi verilerin aktarılabileceği konusunda katı kurallar vardır. Sorun, kapatmanın sağlayıcı referansını "yakalaması" ve bu referansın serileştirilip başka bir izoleye gönderilememesidir.

Sistem, seri hale getirilemeyen verileri göndermeye çalıştığında bu mesajı görürsünüz:

flutter: Error running isolate: Invalid argument(s): Illegal argument in isolate message: object is unsendable - Library:'dart:async' Class: _Future@4048458 (see restrictions listed at `SendPort.send()` documentation for more information)
flutter:  <- Instance of 'AutoDisposeStreamProviderElement<Crossword>' (from package:riverpod/src/stream_provider.dart)
flutter:  <- Context num_variables: 2 <- Context num_variables: 1 parent:{ Context num_variables: 2 }
flutter:  <- Context num_variables: 1 parent:{ Context num_variables: 1 parent:{ Context num_variables: 2 } }
flutter:  <- Closure: () => Crossword? (from package:generate_crossword/providers.dart)

Bu, compute öğesinin bir sağlayıcı üzerinden arka plan yalıtımını kapatmaya devretmesinden kaynaklanır. Bu işlem SendPort.send() üzerinden gönderilemez. Bu sorunu düzeltmek için, kapatma işleminin gönderilemeyen bir öğe üzerinde yapılmadığından emin olun.

İlk adım, sağlayıcıları Isolate kodundan ayırmaktır.

  1. lib dizininizde bir isolates.dart dosyası oluşturun ve aşağıdaki içeriği ekleyin:

lib/isolates.dart

import 'dart:math';

import 'package:built_collection/built_collection.dart';
import 'package:flutter/foundation.dart';

import 'model.dart';
import 'utils.dart';

final _random = Random();

Stream<Crossword> exploreCrosswordSolutions({
  required Crossword crossword,
  required BuiltSet<String> wordList,
}) async* {
  while (crossword.characters.length <
      crossword.width * crossword.height * 0.8) {
    final word = wordList.randomElement();
    final direction = _random.nextBool() ? Direction.across : Direction.down;
    final location = Location.at(
      _random.nextInt(crossword.width),
      _random.nextInt(crossword.height),
    );
    try {
      var candidate = await compute(((String, Direction, Location) wordToAdd) {
        final (word, direction, location) = wordToAdd;
        return crossword.addWord(
          word: word,
          direction: direction,
          location: location,
        );
      }, (word, direction, location));

      if (candidate != null) {
        crossword = candidate;
        yield crossword;
      }
    } catch (e) {
      debugPrint('Error running isolate: $e');
    }
  }
}

Bu kod oldukça tanıdık görünmelidir. Bu işlev, crossword sağlayıcısında bulunan özelliğin temelini oluşturur ancak artık bağımsız bir oluşturma işlevi olarak kullanılabilir. Arka plan yalıtımını başlatmak için bu yeni işlevi kullanmak üzere providers.dart dosyanızı güncelleyebilirsiniz.

lib/providers.dart

// Drop the dart:math import, the _random instance moved to isolates.dart
import 'dart:convert';

import 'package:built_collection/built_collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'isolates.dart';                                    // Add this import
import 'model.dart' as model;
                                                           // Drop the utils.dart import

part 'providers.g.dart';

/// A provider for the wordlist to use when generating the crossword.
@riverpod
Future<BuiltSet<String>> wordList(Ref ref) async {
  // This codebase requires that all words consist of lowercase characters
  // in the range 'a'-'z'. Words containing uppercase letters will be
  // lowercased, and words containing runes outside this range will
  // be removed.

  final re = RegExp(r'^[a-z]+$');
  final words = await rootBundle.loadString('assets/words.txt');
  return const LineSplitter()
      .convert(words)
      .toBuiltSet()
      .rebuild(
        (b) => b
          ..map((word) => word.toLowerCase().trim())
          ..where((word) => word.length > 2)
          ..where((word) => re.hasMatch(word)),
      );
}

/// An enumeration for different sizes of [model.Crossword]s.
enum CrosswordSize {
  small(width: 20, height: 11),
  medium(width: 40, height: 22),
  large(width: 80, height: 44),
  xlarge(width: 160, height: 88),
  xxlarge(width: 500, height: 500);

  const CrosswordSize({required this.width, required this.height});

  final int width;
  final int height;
  String get label => '$width x $height';
}

/// A provider that holds the current size of the crossword to generate.
@Riverpod(keepAlive: true)
class Size extends _$Size {
  var _size = CrosswordSize.medium;

  @override
  CrosswordSize build() => _size;

  void setSize(CrosswordSize size) {
    _size = size;
    ref.invalidateSelf();
  }
}
                                                           // Drop the _random instance
@riverpod
Stream<model.Crossword> crossword(Ref ref) async* {
  final size = ref.watch(sizeProvider);
  final wordListAsync = ref.watch(wordListProvider);

  final emptyCrossword = model.Crossword.crossword(        // Edit from here
    width: size.width,
    height: size.height,
  );

  yield* wordListAsync.when(
    data: (wordList) => exploreCrosswordSolutions(
      crossword: emptyCrossword,
      wordList: wordList,
    ),
    error: (error, stackTrace) async* {
      debugPrint('Error loading word list: $error');
      yield emptyCrossword;
    },
    loading: () async* {
      yield emptyCrossword;                                // To here.
    },
  );
}

Bu sayede, bulmacanın çözülme compute işleminin arka planda gerçekleştiği, farklı boyutlarda kelime bulmacaları oluşturan bir araca sahip olursunuz. Şimdi de kod, bulmacaya hangi kelimelerin ekleneceğine karar verirken daha verimli olabilse.

6. İş sırasını yönetme

Arama Stratejisini Anlama

Bulmaca oluşturma işleminde sistematik bir deneme yanılma yaklaşımı olan geri izleme kullanılır. Önce uygulamanız bir kelimeyi bir konuma yerleştirmeye çalışır, ardından mevcut kelimelerle uyumlu olup olmadığını kontrol eder. Doğruysa kelimeyi saklayın ve sonraki kelimeyi deneyin. Aksi takdirde, cihazı çıkarıp başka bir yerde deneyin.

Geri izleme, her kelime yerleştirme işlemi gelecekteki kelimeler için kısıtlamalar oluşturduğundan ve geçersiz yerleştirmeler hızlı bir şekilde algılanıp bırakıldığından bulmacalarda işe yarar. Değiştirilemeyen veri yapıları, değişiklikleri "geri almayı" verimli hale getirir.

Mevcut kodla ilgili sorunun bir kısmı, çözülen sorunun aslında bir arama sorunu olması ve mevcut çözümün körü körüne arama yapmasıdır. Kod, kelimeleri ızgaranın herhangi bir yerine rastgele yerleştirmeye çalışmak yerine mevcut kelimelere eklenecek kelimeleri bulmaya odaklanırsa sistem çözümleri daha hızlı bulur. Bu soruna yaklaşmanın bir yolu, kelime bulmaya çalışılacak konumların bir iş sırasını oluşturmaktır.

Kod, olası çözümler oluşturur, olası çözümün geçerli olup olmadığını kontrol eder ve geçerliliğe bağlı olarak olası çözümü dahil eder veya atar. Bu, geri izleme algoritma ailesinden bir örnek uygulamadır. Bu uygulama, built_value ve built_collection ile büyük ölçüde kolaylaştırılır. Bu iki özellik, türetildikleri değişmez değerle ortak durumu türeten ve dolayısıyla paylaşan yeni değişmez değerler oluşturulmasını sağlar. Bu sayede, derin kopyalama için gereken bellek maliyetleri olmadan potansiyel adaylardan ucuz bir şekilde yararlanılabilir.

Başlamak için aşağıdaki adımları uygulayın:

  1. model.dart dosyasını açın ve aşağıdaki WorkQueue tanımını ekleyin:

lib/model.dart

  /// Constructor for [Crossword].
  /// Use [Crossword.crossword] instead.
  factory Crossword([void Function(CrosswordBuilder)? updates]) = _$Crossword;
  Crossword._();
}
                                                           // Add from here
/// A work queue for a worker to process. The work queue contains a crossword
/// and a list of locations to try, along with candidate words to add to the
/// crossword.
abstract class WorkQueue implements Built<WorkQueue, WorkQueueBuilder> {
  static Serializer<WorkQueue> get serializer => _$workQueueSerializer;

  /// The crossword the worker is working on.
  Crossword get crossword;

  /// The outstanding queue of locations to try.
  BuiltMap<Location, Direction> get locationsToTry;

  /// Known bad locations.
  BuiltSet<Location> get badLocations;

  /// The list of unused candidate words that can be added to this crossword.
  BuiltSet<String> get candidateWords;

  /// Returns true if the work queue is complete.
  bool get isCompleted => locationsToTry.isEmpty || candidateWords.isEmpty;

  /// Create a work queue from a crossword.
  static WorkQueue from({
    required Crossword crossword,
    required Iterable<String> candidateWords,
    required Location startLocation,
  }) => WorkQueue((b) {
    if (crossword.words.isEmpty) {
      // Strip candidate words too long to fit in the crossword
      b.candidateWords.addAll(
        candidateWords.where(
          (word) => word.characters.length <= crossword.width,
        ),
      );

      b.crossword.replace(crossword);

      b.locationsToTry.addAll({startLocation: Direction.across});
    } else {
      // Assuming words have already been stripped to length
      b.candidateWords.addAll(
        candidateWords.toBuiltSet().rebuild(
          (b) => b.removeAll(crossword.words.map((word) => word.word)),
        ),
      );
      b.crossword.replace(crossword);
      crossword.characters
          .rebuild(
            (b) => b.removeWhere((location, character) {
              if (character.acrossWord != null && character.downWord != null) {
                return true;
              }
              final left = crossword.characters[location.left];
              if (left != null && left.downWord != null) return true;
              final right = crossword.characters[location.right];
              if (right != null && right.downWord != null) return true;
              final up = crossword.characters[location.up];
              if (up != null && up.acrossWord != null) return true;
              final down = crossword.characters[location.down];
              if (down != null && down.acrossWord != null) return true;
              return false;
            }),
          )
          .forEach((location, character) {
            b.locationsToTry.addAll({
              location: switch ((character.acrossWord, character.downWord)) {
                (null, null) => throw StateError(
                  'Character is not part of a word',
                ),
                (null, _) => Direction.across,
                (_, null) => Direction.down,
                (_, _) => throw StateError('Character is part of two words'),
              },
            });
          });
    }
  });

  WorkQueue remove(Location location) => rebuild(
    (b) => b
      ..locationsToTry.remove(location)
      ..badLocations.add(location),
  );

  /// Update the work queue from a crossword derived from the current crossword
  /// that this work queue is built from.
  WorkQueue updateFrom(final Crossword crossword) =>
      WorkQueue.from(
        crossword: crossword,
        candidateWords: candidateWords,
        startLocation: locationsToTry.isNotEmpty
            ? locationsToTry.keys.first
            : Location.at(0, 0),
      ).rebuild(
        (b) => b
          ..badLocations.addAll(badLocations)
          ..locationsToTry.removeWhere(
            (location, _) => badLocations.contains(location),
          ),
      );

  /// Factory constructor for [WorkQueue]
  factory WorkQueue([void Function(WorkQueueBuilder)? updates]) = _$WorkQueue;

  WorkQueue._();
}                                                          // To here.

/// Construct the serialization/deserialization code for the data model.
@SerializersFor([
  Location,
  Crossword,
  CrosswordWord,
  CrosswordCharacter,
  WorkQueue,                                               // Add this line
])
final Serializers serializers = _$serializers;
  1. Bu yeni içeriği ekledikten sonra bu dosyada birkaç saniyeden uzun süre kırmızı dalgalı çizgiler kalırsa build_runner uygulamanızın hâlâ çalıştığını doğrulayın. Görmüyorsanız dart run build_runner watch -d komutunu çalıştırın.

Çeşitli boyutlarda bulmaca oluşturmanın ne kadar sürdüğünü göstermek için günlüğe kaydetme işlevini koda ekleyeceksiniz. Sürelerin güzel biçimlendirilmiş bir gösterimi olsa iyi olurdu. Neyse ki uzantı yöntemleriyle tam olarak ihtiyacımız olan yöntemi ekleyebiliriz.

  1. utils.dart dosyasını aşağıdaki gibi düzenleyin:

lib/utils.dart

import 'dart:math';

import 'package:built_collection/built_collection.dart';

/// A [Random] instance for generating random numbers.
final _random = Random();

/// An extension on [BuiltSet] that adds a method to get a random element.
extension RandomElements<E> on BuiltSet<E> {
  E randomElement() {
    return elementAt(_random.nextInt(length));
  }
}
                                                              // Add from here
/// An extension on [Duration] that adds a method to format the duration.
extension DurationFormat on Duration {
  /// A human-readable string representation of the duration.
  /// This format is tuned for durations in the seconds to days range.
  String get formatted {
    final hours = inHours.remainder(24).toString().padLeft(2, '0');
    final minutes = inMinutes.remainder(60).toString().padLeft(2, '0');
    final seconds = inSeconds.remainder(60).toString().padLeft(2, '0');
    return switch ((inDays, inHours, inMinutes, inSeconds)) {
      (0, 0, 0, _) => '${inSeconds}s',
      (0, 0, _, _) => '$inMinutes:$seconds',
      (0, _, _, _) => '$inHours:$minutes:$seconds',
      _ => '$inDays days, $hours:$minutes:$seconds',
    };
  }
}                                                             // To here.

Bu uzatma yöntemi, saniyelerden günlere kadar değişen farklı süreleri görüntülemenin uygun yolunu seçmek için kayıtlar üzerinde anahtar ifadelerden ve kalıp eşleştirmeden yararlanır. Bu kod stili hakkında daha fazla bilgi için Dive into Dart's patterns and records (Dart'ın kalıplarına ve kayıtlarına dalın) adlı codelab'i inceleyin.

  1. Bu yeni işlevi entegre etmek için isolates.dart işlevinin nasıl tanımlandığını aşağıdaki şekilde yeniden tanımlamak üzere isolates.dart dosyasını değiştirin:exploreCrosswordSolutions

lib/isolates.dart

import 'package:built_collection/built_collection.dart';
import 'package:characters/characters.dart';
import 'package:flutter/foundation.dart';

import 'model.dart';
import 'utils.dart';

Stream<Crossword> exploreCrosswordSolutions({
  required Crossword crossword,
  required BuiltSet<String> wordList,
}) async* {
  final start = DateTime.now();
  var workQueue = WorkQueue.from(
    crossword: crossword,
    candidateWords: wordList,
    startLocation: Location.at(0, 0),
  );
  while (!workQueue.isCompleted) {
    final location = workQueue.locationsToTry.keys.toBuiltSet().randomElement();
    try {
      final crossword = await compute(((WorkQueue, Location) workMessage) {
        final (workQueue, location) = workMessage;
        final direction = workQueue.locationsToTry[location]!;
        final target = workQueue.crossword.characters[location];
        if (target == null) {
          return workQueue.crossword.addWord(
            direction: direction,
            location: location,
            word: workQueue.candidateWords.randomElement(),
          );
        }
        var words = workQueue.candidateWords.toBuiltList().rebuild(
          (b) => b
            ..where((b) => b.characters.contains(target.character))
            ..shuffle(),
        );
        int tryCount = 0;
        for (final word in words) {
          tryCount++;
          for (final (index, character) in word.characters.indexed) {
            if (character != target.character) continue;

            final candidate = workQueue.crossword.addWord(
              location: switch (direction) {
                Direction.across => location.leftOffset(index),
                Direction.down => location.upOffset(index),
              },
              word: word,
              direction: direction,
            );
            if (candidate != null) {
              return candidate;
            }
          }
          if (tryCount > 1000) {
            break;
          }
        }
      }, (workQueue, location));
      if (crossword != null) {
        workQueue = workQueue.updateFrom(crossword);
        yield crossword;
      } else {
        workQueue = workQueue.remove(location);
      }
    } catch (e) {
      debugPrint('Error running isolate: $e');
    }
  }
  debugPrint(
    '${crossword.width} x ${crossword.height} Crossword generated in '
    '${DateTime.now().difference(start).formatted}',
  );
}

Bu kodu çalıştırdığınızda, görünüşte aynı olan bir uygulama elde edersiniz. Ancak fark, tamamlanmış bir bulmacayı bulmanın ne kadar sürdüğüdür. Aşağıda, 1 dakika 29 saniyede oluşturulan 80 x 44 boyutlarında bir bulmaca yer almaktadır.

Kontrol noktası: Verimli algoritma çalışması

Bulmaca oluşturma işlemi artık şu nedenlerle önemli ölçüde daha hızlı:

  • Akıllı kelime yerleşimi hedefleme kesişim noktaları
  • Yerleşimler başarısız olduğunda verimli geri izleme
  • Gereksiz aramalardan kaçınmak için iş sırası yönetimi

Birçok kelimenin kesiştiği bulmaca oluşturucu. Uzaklaştırıldığında kelimeler okunamayacak kadar küçük.

Tabii ki akla gelen soru, daha hızlı gidebilir miyiz? Evet, yapabiliriz.

7. Yüzey istatistikleri

Neden İstatistik Eklemelisiniz?

Bir şeyi hızlı bir şekilde yapmak için neler olduğunu görmek yardımcı olur. İstatistikler, algoritmanın performansını anlık olarak görmenizi sağlayarak ilerlemeyi izlemenize yardımcı olur. Bu sayede, algoritmanın zamanını nerede harcadığını anlayarak darboğazları belirleyebilirsiniz. Bu sayede, optimizasyon yaklaşımları hakkında bilinçli kararlar vererek performansı ayarlayabilirsiniz.

Göstereceğiniz bilgilerin WorkQueue'dan çıkarılması ve kullanıcı arayüzünde gösterilmesi gerekir. Faydalı bir ilk adım, göstermek istediğiniz bilgileri içeren yeni bir model sınıfı tanımlamaktır.

Başlamak için aşağıdaki adımları uygulayın:

  1. DisplayInfo sınıfını eklemek için model.dart dosyasını aşağıdaki gibi düzenleyin:

lib/model.dart

import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:characters/characters.dart';
import 'package:intl/intl.dart';                           // Add this import

part 'model.g.dart';

/// A location in a crossword.
abstract class Location implements Built<Location, LocationBuilder> {
  1. Dosyanın sonunda, DisplayInfo sınıfını eklemek için aşağıdaki değişiklikleri yapın:

lib/model.dart

  /// Factory constructor for [WorkQueue]
  factory WorkQueue([void Function(WorkQueueBuilder)? updates]) = _$WorkQueue;

  WorkQueue._();
}
                                                           // Add from here.
/// Display information for the current state of the crossword solve.
abstract class DisplayInfo implements Built<DisplayInfo, DisplayInfoBuilder> {
  static Serializer<DisplayInfo> get serializer => _$displayInfoSerializer;

  /// The number of words in the grid.
  String get wordsInGridCount;

  /// The number of candidate words.
  String get candidateWordsCount;

  /// The number of locations to explore.
  String get locationsToExploreCount;

  /// The number of known bad locations.
  String get knownBadLocationsCount;

  /// The percentage of the grid filled.
  String get gridFilledPercentage;

  /// Construct a [DisplayInfo] instance from a [WorkQueue].
  factory DisplayInfo.from({required WorkQueue workQueue}) {
    final gridFilled =
        (workQueue.crossword.characters.length /
        (workQueue.crossword.width * workQueue.crossword.height));
    final fmt = NumberFormat.decimalPattern();

    return DisplayInfo(
      (b) => b
        ..wordsInGridCount = fmt.format(workQueue.crossword.words.length)
        ..candidateWordsCount = fmt.format(workQueue.candidateWords.length)
        ..locationsToExploreCount = fmt.format(workQueue.locationsToTry.length)
        ..knownBadLocationsCount = fmt.format(workQueue.badLocations.length)
        ..gridFilledPercentage = '${(gridFilled * 100).toStringAsFixed(2)}%',
    );
  }

  /// An empty [DisplayInfo] instance.
  static DisplayInfo get empty => DisplayInfo(
    (b) => b
      ..wordsInGridCount = '0'
      ..candidateWordsCount = '0'
      ..locationsToExploreCount = '0'
      ..knownBadLocationsCount = '0'
      ..gridFilledPercentage = '0%',
  );

  factory DisplayInfo([void Function(DisplayInfoBuilder)? updates]) =
      _$DisplayInfo;
  DisplayInfo._();
}                                                          // To here.

/// Construct the serialization/deserialization code for the data model.
@SerializersFor([
  Location,
  Crossword,
  CrosswordWord,
  CrosswordCharacter,
  WorkQueue,
  DisplayInfo,                                             // Add this line.
])
final Serializers serializers = _$serializers;
  1. isolates.dart dosyasını, WorkQueue modelini aşağıdaki şekilde gösterecek şekilde değiştirin:

lib/isolates.dart

import 'package:built_collection/built_collection.dart';
import 'package:characters/characters.dart';
import 'package:flutter/foundation.dart';

import 'model.dart';
import 'utils.dart';

Stream<WorkQueue> exploreCrosswordSolutions({              // Modify this line
  required Crossword crossword,
  required BuiltSet<String> wordList,
}) async* {
  final start = DateTime.now();
  var workQueue = WorkQueue.from(
    crossword: crossword,
    candidateWords: wordList,
    startLocation: Location.at(0, 0),
  );
  while (!workQueue.isCompleted) {
    final location = workQueue.locationsToTry.keys.toBuiltSet().randomElement();
    try {
      final crossword = await compute(((WorkQueue, Location) workMessage) {
        final (workQueue, location) = workMessage;
        final direction = workQueue.locationsToTry[location]!;
        final target = workQueue.crossword.characters[location];
        if (target == null) {
          return workQueue.crossword.addWord(
            direction: direction,
            location: location,
            word: workQueue.candidateWords.randomElement(),
          );
        }
        var words = workQueue.candidateWords.toBuiltList().rebuild(
          (b) => b
            ..where((b) => b.characters.contains(target.character))
            ..shuffle(),
        );
        int tryCount = 0;
        for (final word in words) {
          tryCount++;
          for (final (index, character) in word.characters.indexed) {
            if (character != target.character) continue;

            final candidate = workQueue.crossword.addWord(
              location: switch (direction) {
                Direction.across => location.leftOffset(index),
                Direction.down => location.upOffset(index),
              },
              word: word,
              direction: direction,
            );
            if (candidate != null) {
              return candidate;
            }
          }
          if (tryCount > 1000) {
            break;
          }
        }
      }, (workQueue, location));
      if (crossword != null) {
        workQueue = workQueue.updateFrom(crossword);       // Drop the yield crossword;
      } else {
        workQueue = workQueue.remove(location);
      }
      yield workQueue;                                     // Add this line.
    } catch (e) {
      debugPrint('Error running isolate: $e');
    }
  }
  debugPrint(
    '${crossword.width} x ${crossword.height} Crossword generated in '
    '${DateTime.now().difference(start).formatted}',
  );
}

Arka plan izolasyonu artık iş kuyruğunu ortaya çıkardığına göre, bu veri kaynağından istatistiklerin nasıl ve nereden elde edileceği sorusu gündeme geliyor.

  1. Eski bulmaca sağlayıcıyı bir iş sırası sağlayıcıyla değiştirin ve ardından iş sırası sağlayıcının akışından bilgi alan başka sağlayıcılar ekleyin:

lib/providers.dart

import 'dart:convert';
import 'dart:math';                                        // Add this import

import 'package:built_collection/built_collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'isolates.dart';
import 'model.dart' as model;

part 'providers.g.dart';

/// A provider for the wordlist to use when generating the crossword.
@riverpod
Future<BuiltSet<String>> wordList(Ref ref) async {
  // This codebase requires that all words consist of lowercase characters
  // in the range 'a'-'z'. Words containing uppercase letters will be
  // lowercased, and words containing runes outside this range will
  // be removed.

  final re = RegExp(r'^[a-z]+$');
  final words = await rootBundle.loadString('assets/words.txt');
  return const LineSplitter()
      .convert(words)
      .toBuiltSet()
      .rebuild(
        (b) => b
          ..map((word) => word.toLowerCase().trim())
          ..where((word) => word.length > 2)
          ..where((word) => re.hasMatch(word)),
      );
}

/// An enumeration for different sizes of [model.Crossword]s.
enum CrosswordSize {
  small(width: 20, height: 11),
  medium(width: 40, height: 22),
  large(width: 80, height: 44),
  xlarge(width: 160, height: 88),
  xxlarge(width: 500, height: 500);

  const CrosswordSize({required this.width, required this.height});

  final int width;
  final int height;
  String get label => '$width x $height';
}

/// A provider that holds the current size of the crossword to generate.
@Riverpod(keepAlive: true)
class Size extends _$Size {
  var _size = CrosswordSize.medium;

  @override
  CrosswordSize build() => _size;

  void setSize(CrosswordSize size) {
    _size = size;
    ref.invalidateSelf();
  }
}

@riverpod
Stream<model.WorkQueue> workQueue(Ref ref) async* {        // Modify this provider
  final size = ref.watch(sizeProvider);
  final wordListAsync = ref.watch(wordListProvider);
  final emptyCrossword = model.Crossword.crossword(
    width: size.width,
    height: size.height,
  );
  final emptyWorkQueue = model.WorkQueue.from(
    crossword: emptyCrossword,
    candidateWords: BuiltSet<String>(),
    startLocation: model.Location.at(0, 0),
  );

  ref.read(startTimeProvider.notifier).start();
  ref.read(endTimeProvider.notifier).clear();

  yield* wordListAsync.when(
    data: (wordList) => exploreCrosswordSolutions(
      crossword: emptyCrossword,
      wordList: wordList,
    ),
    error: (error, stackTrace) async* {
      debugPrint('Error loading word list: $error');
      yield emptyWorkQueue;
    },
    loading: () async* {
      yield emptyWorkQueue;
    },
  );

  ref.read(endTimeProvider.notifier).end();
}                                                          // To here.

@Riverpod(keepAlive: true)                                 // Add from here to end of file
class StartTime extends _$StartTime {
  @override
  DateTime? build() => _start;

  DateTime? _start;

  void start() {
    _start = DateTime.now();
    ref.invalidateSelf();
  }
}

@Riverpod(keepAlive: true)
class EndTime extends _$EndTime {
  @override
  DateTime? build() => _end;

  DateTime? _end;

  void clear() {
    _end = null;
    ref.invalidateSelf();
  }

  void end() {
    _end = DateTime.now();
    ref.invalidateSelf();
  }
}

const _estimatedTotalCoverage = 0.54;

@riverpod
Duration expectedRemainingTime(Ref ref) {
  final startTime = ref.watch(startTimeProvider);
  final endTime = ref.watch(endTimeProvider);
  final workQueueAsync = ref.watch(workQueueProvider);

  return workQueueAsync.when(
    data: (workQueue) {
      if (startTime == null || endTime != null || workQueue.isCompleted) {
        return Duration.zero;
      }
      try {
        final soFar = DateTime.now().difference(startTime);
        final completedPercentage = min(
          0.99,
          (workQueue.crossword.characters.length /
              (workQueue.crossword.width * workQueue.crossword.height) /
              _estimatedTotalCoverage),
        );
        final expectedTotal = soFar.inSeconds / completedPercentage;
        final expectedRemaining = expectedTotal - soFar.inSeconds;
        return Duration(seconds: expectedRemaining.toInt());
      } catch (e) {
        return Duration.zero;
      }
    },
    error: (error, stackTrace) => Duration.zero,
    loading: () => Duration.zero,
  );
}

/// A provider that holds whether to display info.
@Riverpod(keepAlive: true)
class ShowDisplayInfo extends _$ShowDisplayInfo {
  var _display = true;

  @override
  bool build() => _display;

  void toggle() {
    _display = !_display;
    ref.invalidateSelf();
  }
}

/// A provider that summarise the DisplayInfo from a [model.WorkQueue].
@riverpod
class DisplayInfo extends _$DisplayInfo {
  @override
  model.DisplayInfo build() => ref
      .watch(workQueueProvider)
      .when(
        data: (workQueue) => model.DisplayInfo.from(workQueue: workQueue),
        error: (error, stackTrace) => model.DisplayInfo.empty,
        loading: () => model.DisplayInfo.empty,
      );
}

Yeni sağlayıcılar, bilgilerin bulmaca ızgarasının üzerine yerleştirilip yerleştirilmeyeceği şeklinde küresel bir durum ve bulmaca oluşturma süresi gibi türetilmiş verilerden oluşur. Tüm bunlar, bu durumun bazı dinleyicilerinin geçici olması nedeniyle daha da karmaşık hale gelir. Bilgi ekranı gizliyse hiçbir şey bulmaca hesaplamasının başlangıç ve bitiş zamanlarını dinlemez ancak bilgi ekranı gösterildiğinde hesaplamanın doğru olması için bu zamanların bellekte kalması gerekir. Bu durumda Riverpod özelliğinin keepAlive parametresi çok faydalıdır.

Bilgi ekranının gösterilmesinde küçük bir sorun var. Geçen çalışma süresini göstermek istiyoruz ancak geçen sürenin sürekli güncellenmesini zorlayacak bir şey yok. Building next generation UIs in Flutter (Flutter'da yeni nesil kullanıcı arayüzleri oluşturma) adlı codelab'e geri dönecek olursak bu gereksinim için kullanışlı bir widget'ı burada bulabilirsiniz.

  1. lib/widgets dizininde bir ticker_builder.dart dosyası oluşturup aşağıdaki içeriği ekleyin:

lib/widgets/ticker_builder.dart

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

/// A Builder widget that invokes its [builder] function on every animation frame.
class TickerBuilder extends StatefulWidget {
  const TickerBuilder({super.key, required this.builder});
  final Widget Function(BuildContext context) builder;
  @override
  State<TickerBuilder> createState() => _TickerBuilderState();
}

class _TickerBuilderState extends State<TickerBuilder>
    with SingleTickerProviderStateMixin {
  late final Ticker _ticker;

  @override
  void initState() {
    super.initState();
    _ticker = createTicker(_handleTick)..start();
  }

  @override
  void dispose() {
    _ticker.dispose();
    super.dispose();
  }

  void _handleTick(Duration elapsed) {
    setState(() {
      // Force a rebuild without changing the widget tree.
    });
  }

  @override
  Widget build(BuildContext context) => widget.builder.call(context);
}

Bu widget bir balyozdur. İçeriğini her karede yeniden oluşturur. Bu genellikle uygun görülmez ancak kelime bulmacaları aramanın hesaplama yüküyle karşılaştırıldığında, geçen süreyi her karede yeniden boyamanın hesaplama yükü muhtemelen gürültü içinde kaybolur. Bu yeni elde edilen bilgilerden yararlanmak için yeni bir widget oluşturmanın zamanı geldi.

  1. lib/widgets dizininizde bir crossword_info_widget.dart dosyası oluşturun ve aşağıdaki içeriği bu dosyaya ekleyin:

lib/widgets/crossword_info_widget.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../providers.dart';
import '../utils.dart';
import 'ticker_builder.dart';

class CrosswordInfoWidget extends ConsumerWidget {
  const CrosswordInfoWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final size = ref.watch(sizeProvider);
    final displayInfo = ref.watch(displayInfoProvider);
    final startTime = ref.watch(startTimeProvider);
    final endTime = ref.watch(endTimeProvider);
    final remaining = ref.watch(expectedRemainingTimeProvider);

    return Align(
      alignment: Alignment.bottomRight,
      child: Padding(
        padding: const EdgeInsets.only(right: 32.0, bottom: 32.0),
        child: ClipRRect(
          borderRadius: BorderRadius.circular(8),
          child: ColoredBox(
            color: Theme.of(context).colorScheme.onPrimary.withAlpha(230),
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
              child: DefaultTextStyle(
                style: TextStyle(
                  fontSize: 16,
                  color: Theme.of(context).colorScheme.primary,
                ),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    _CrosswordInfoRichText(
                      label: 'Grid Size',
                      value: '${size.width} x ${size.height}',
                    ),
                    _CrosswordInfoRichText(
                      label: 'Words in grid',
                      value: displayInfo.wordsInGridCount,
                    ),
                    _CrosswordInfoRichText(
                      label: 'Candidate words',
                      value: displayInfo.candidateWordsCount,
                    ),
                    _CrosswordInfoRichText(
                      label: 'Locations to explore',
                      value: displayInfo.locationsToExploreCount,
                    ),
                    _CrosswordInfoRichText(
                      label: 'Known bad locations',
                      value: displayInfo.knownBadLocationsCount,
                    ),
                    _CrosswordInfoRichText(
                      label: 'Grid filled',
                      value: displayInfo.gridFilledPercentage,
                    ),
                    switch ((startTime, endTime)) {
                      (null, _) => _CrosswordInfoRichText(
                        label: 'Time elapsed',
                        value: 'Not started yet',
                      ),
                      (DateTime start, null) => TickerBuilder(
                        builder: (context) => _CrosswordInfoRichText(
                          label: 'Time elapsed',
                          value: DateTime.now().difference(start).formatted,
                        ),
                      ),
                      (DateTime start, DateTime end) => _CrosswordInfoRichText(
                        label: 'Completed in',
                        value: end.difference(start).formatted,
                      ),
                    },
                    if (startTime != null && endTime == null)
                      _CrosswordInfoRichText(
                        label: 'Est. remaining',
                        value: remaining.formatted,
                      ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _CrosswordInfoRichText extends StatelessWidget {
  final String label;
  final String value;

  const _CrosswordInfoRichText({required this.label, required this.value});

  @override
  Widget build(BuildContext context) => RichText(
    text: TextSpan(
      children: [
        TextSpan(text: '$label ', style: DefaultTextStyle.of(context).style),
        TextSpan(
          text: value,
          style: DefaultTextStyle.of(
            context,
          ).style.copyWith(fontWeight: FontWeight.bold),
        ),
      ],
    ),
  );
}

Bu widget, Riverpod'un sağlayıcılarının gücüne dair mükemmel bir örnektir. Beş sağlayıcıdan herhangi biri güncellendiğinde bu widget yeniden oluşturulmak üzere işaretlenir. Bu adımda yapılması gereken son değişiklik, bu yeni widget'ı kullanıcı arayüzüne entegre etmektir.

  1. crossword_generator_app.dart dosyanızı aşağıdaki gibi düzenleyin:

lib/widgets/crossword_generator_app.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../providers.dart';
import 'crossword_info_widget.dart';                       // Add this import
import 'crossword_widget.dart';

class CrosswordGeneratorApp extends StatelessWidget {
  const CrosswordGeneratorApp({super.key});

  @override
  Widget build(BuildContext context) {
    return _EagerInitialization(
      child: Scaffold(
        appBar: AppBar(
          actions: [_CrosswordGeneratorMenu()],
          titleTextStyle: TextStyle(
            color: Theme.of(context).colorScheme.primary,
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
          title: Text('Crossword Generator'),
        ),
        body: SafeArea(
          child: Consumer(                                 // Modify from here
            builder: (context, ref, child) {
              return Stack(
                children: [
                  Positioned.fill(child: CrosswordWidget()),
                  if (ref.watch(showDisplayInfoProvider)) CrosswordInfoWidget(),
                ],
              );
            },
          ),                                               // To here.
        ),
      ),
    );
  }
}

class _EagerInitialization extends ConsumerWidget {
  const _EagerInitialization({required this.child});
  final Widget child;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.watch(wordListProvider);
    return child;
  }
}

class _CrosswordGeneratorMenu extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) => MenuAnchor(
    menuChildren: [
      for (final entry in CrosswordSize.values)
        MenuItemButton(
          onPressed: () => ref.read(sizeProvider.notifier).setSize(entry),
          leadingIcon: entry == ref.watch(sizeProvider)
              ? Icon(Icons.radio_button_checked_outlined)
              : Icon(Icons.radio_button_unchecked_outlined),
          child: Text(entry.label),
        ),
      MenuItemButton(                                      // Add from here
        leadingIcon: ref.watch(showDisplayInfoProvider)
            ? Icon(Icons.check_box_outlined)
            : Icon(Icons.check_box_outline_blank_outlined),
        onPressed: () => ref.read(showDisplayInfoProvider.notifier).toggle(),
        child: Text('Display Info'),
      ),                                                   // To here.
    ],
    builder: (context, controller, child) => IconButton(
      onPressed: () => controller.open(),
      icon: Icon(Icons.settings),
    ),
  );
}

Buradaki iki değişiklik, sağlayıcıları entegre etmeye yönelik farklı yaklaşımları gösterir. CrosswordGeneratorApp'nın build yönteminde, bilgi ekranı gösterildiğinde veya gizlendiğinde yeniden oluşturulması zorunlu olan alanı içeren yeni bir Consumer oluşturucu tanıttınız. Diğer yandan, açılır menünün tamamı tek bir ConsumerWidget'dır. Bu ConsumerWidget, bulmacanın yeniden boyutlandırılması veya bilgi ekranının gösterilmesi ya da gizlenmesi durumunda yeniden oluşturulur. Hangi yaklaşımın benimseneceği, her zaman basitlik ile yeniden oluşturulan widget ağaçlarının düzenlerini yeniden hesaplamanın maliyeti arasında bir mühendislik değiş tokuşudur.

Uygulamayı çalıştırmak artık kullanıcıya bulmaca oluşturma sürecinin nasıl ilerlediği hakkında daha fazla bilgi veriyor. Ancak bulmaca oluşturma işleminin sonuna doğru, sayıların değiştiği ancak karakter ızgarasında çok az değişiklik olduğu bir dönem görüyoruz.

Bulmaca Oluşturucu uygulama penceresi (bu kez daha küçük, tanınabilir kelimeler ve sağ alt köşede, mevcut oluşturma çalıştırmasıyla ilgili istatistiklerin yer aldığı kayan bir katman)

Ne olduğunu ve neden olduğunu anlamak için ek bilgiler edinmek faydalı olacaktır.

8. İş parçacıklarıyla paralelleştirme

Performansın düşmesinin nedenleri

Bulmaca tamamlanmaya yaklaştıkça geçerli kelime yerleştirme seçenekleri azaldığı için algoritma yavaşlar. Algoritma, çalışmayacak birçok kombinasyonu dener. Tek iş parçacıklı işleme, birden fazla seçeneği verimli bir şekilde keşfedemez.

Algoritmayı Görselleştirme

İşlemlerin neden yavaşladığını anlamak için algoritmanın ne yaptığını görselleştirmek faydalı olur. Önemli bir bölüm, WorkQueue içindeki olağanüstü locationsToTry'dir. TableView, bu durumu incelemek için kullanışlı bir yol sunar. Hücrenin locationsToTry içinde olup olmamasına göre hücre rengini değiştirebiliriz.

Başlamak için aşağıdaki adımları uygulayın:

  1. crossword_widget.dart dosyasını şu şekilde değiştirin:

lib/widgets/crossword_widget.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart';

import '../model.dart';
import '../providers.dart';

class CrosswordWidget extends ConsumerWidget {
  const CrosswordWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final size = ref.watch(sizeProvider);
    return TableView.builder(
      diagonalDragBehavior: DiagonalDragBehavior.free,
      cellBuilder: _buildCell,
      columnCount: size.width,
      columnBuilder: (index) => _buildSpan(context, index),
      rowCount: size.height,
      rowBuilder: (index) => _buildSpan(context, index),
    );
  }

  TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) {
    final location = Location.at(vicinity.column, vicinity.row);

    return TableViewCell(
      child: Consumer(
        builder: (context, ref, _) {
          final character = ref.watch(
            workQueueProvider.select(
              (workQueueAsync) => workQueueAsync.when(
                data: (workQueue) => workQueue.crossword.characters[location],
                error: (error, stackTrace) => null,
                loading: () => null,
              ),
            ),
          );

          final explorationCell = ref.watch(               // Add from here
            workQueueProvider.select(
              (workQueueAsync) => workQueueAsync.when(
                data: (workQueue) =>
                    workQueue.locationsToTry.keys.contains(location),
                error: (error, stackTrace) => false,
                loading: () => false,
              ),
            ),
          );                                               // To here.

          if (character != null) {                         // Modify from here
            return AnimatedContainer(
              duration: Durations.extralong1,
              curve: Curves.easeInOut,
              color: explorationCell
                  ? Theme.of(context).colorScheme.primary
                  : Theme.of(context).colorScheme.onPrimary,
              child: Center(
                child: AnimatedDefaultTextStyle(
                  duration: Durations.extralong1,
                  curve: Curves.easeInOut,
                  style: TextStyle(
                    fontSize: 24,
                    color: explorationCell
                        ? Theme.of(context).colorScheme.onPrimary
                        : Theme.of(context).colorScheme.primary,
                  ),
                  child: Text(character.character),
                ),                                          // To here.
              ),
            );
          }

          return ColoredBox(
            color: Theme.of(context).colorScheme.primaryContainer,
          );
        },
      ),
    );
  }

  TableSpan _buildSpan(BuildContext context, int index) {
    return TableSpan(
      extent: FixedTableSpanExtent(32),
      foregroundDecoration: TableSpanDecoration(
        border: TableSpanBorder(
          leading: BorderSide(
            color: Theme.of(context).colorScheme.onPrimaryContainer,
          ),
          trailing: BorderSide(
            color: Theme.of(context).colorScheme.onPrimaryContainer,
          ),
        ),
      ),
    );
  }
}

Bu kodu çalıştırdığınızda, algoritmanın henüz incelemediği, öne çıkan konumların görselleştirilmiş halini görürsünüz.

Üretim sürecinin ortasında olan Çapraz Bulmaca Oluşturucu Bazı harflerde koyu mavi arka plan üzerinde beyaz metin, bazılarında ise beyaz arka plan üzerinde mavi metin bulunur.

Bulmaca tamamlanmaya doğru ilerlerken bunu izlemenin ilginç yanı, araştırılacak bir dizi nokta kalmasıdır ancak bu noktalar faydalı bir sonuç vermez. Burada iki seçenek vardır: Birincisi, bulmaca hücrelerinin belirli bir yüzdesi doldurulduktan sonra araştırmayı sınırlamak, ikincisi ise aynı anda birden fazla ilgi alanını incelemektir. İkinci yol daha eğlenceli görünüyor. O zaman bu yolu deneyelim.

  1. isolates.dart dosyasını düzenleyin. Bu, bir arka plan yalıtımında hesaplananları N arka plan yalıtımından oluşan bir havuza ayırmak için kodun neredeyse tamamen yeniden yazılmasıdır.

lib/isolates.dart

import 'package:built_collection/built_collection.dart';
import 'package:characters/characters.dart';
import 'package:flutter/foundation.dart';

import 'model.dart';
import 'utils.dart';

Stream<WorkQueue> exploreCrosswordSolutions({
  required Crossword crossword,
  required BuiltSet<String> wordList,
  required int maxWorkerCount,
}) async* {
  final start = DateTime.now();
  var workQueue = WorkQueue.from(
    crossword: crossword,
    candidateWords: wordList,
    startLocation: Location.at(0, 0),
  );
  while (!workQueue.isCompleted) {
    try {
      workQueue = await compute(_generate, (workQueue, maxWorkerCount));
      yield workQueue;
    } catch (e) {
      debugPrint('Error running isolate: $e');
    }
  }

  debugPrint(
    'Generated ${workQueue.crossword.width} x '
    '${workQueue.crossword.height} crossword in '
    '${DateTime.now().difference(start).formatted} '
    'with $maxWorkerCount workers.',
  );
}

Future<WorkQueue> _generate((WorkQueue, int) workMessage) async {
  var (workQueue, maxWorkerCount) = workMessage;
  final candidateGeneratorFutures = <Future<(Location, Direction, String?)>>[];
  final locations = workQueue.locationsToTry.keys.toBuiltList().rebuild(
    (b) => b
      ..shuffle()
      ..take(maxWorkerCount),
  );

  for (final location in locations) {
    final direction = workQueue.locationsToTry[location]!;

    candidateGeneratorFutures.add(
      compute(_generateCandidate, (
        workQueue.crossword,
        workQueue.candidateWords,
        location,
        direction,
      )),
    );
  }

  try {
    final results = await candidateGeneratorFutures.wait;
    var crossword = workQueue.crossword;
    for (final (location, direction, word) in results) {
      if (word != null) {
        final candidate = crossword.addWord(
          location: location,
          word: word,
          direction: direction,
        );
        if (candidate != null) {
          crossword = candidate;
        }
      } else {
        workQueue = workQueue.remove(location);
      }
    }

    workQueue = workQueue.updateFrom(crossword);
  } catch (e) {
    debugPrint('$e');
  }

  return workQueue;
}

(Location, Direction, String?) _generateCandidate(
  (Crossword, BuiltSet<String>, Location, Direction) searchDetailMessage,
) {
  final (crossword, candidateWords, location, direction) = searchDetailMessage;

  final target = crossword.characters[location];
  if (target == null) {
    return (location, direction, candidateWords.randomElement());
  }

  // Filter down the candidate word list to those that contain the letter
  // at the current location
  final words = candidateWords.toBuiltList().rebuild(
    (b) => b
      ..where((b) => b.characters.contains(target.character))
      ..shuffle(),
  );
  int tryCount = 0;
  final start = DateTime.now();
  for (final word in words) {
    tryCount++;
    for (final (index, character) in word.characters.indexed) {
      if (character != target.character) continue;

      final candidate = crossword.addWord(
        location: switch (direction) {
          Direction.across => location.leftOffset(index),
          Direction.down => location.upOffset(index),
        },
        word: word,
        direction: direction,
      );
      if (candidate != null) {
        return switch (direction) {
          Direction.across => (location.leftOffset(index), direction, word),
          Direction.down => (location.upOffset(index), direction, word),
        };
      }
      final deltaTime = DateTime.now().difference(start);
      if (tryCount >= 1000 || deltaTime > Duration(seconds: 10)) {
        return (location, direction, null);
      }
    }
  }

  return (location, direction, null);
}

Çoklu yalıtım mimarisini anlama

Temel işletme mantığı değişmediğinden bu kodun çoğu tanıdık gelecektir. Değişen şey, artık iki katmanlı compute çağrıları olmasıdır. İlk katman, arama için N sayıda çalışan yalıtılmış birime ayrı ayrı konumlar atamaktan ve N sayıda çalışan yalıtılmış birimin tamamı bittiğinde sonuçları yeniden birleştirmekten sorumludur. İkinci katman, N çalışan yalıtılmış alanından oluşur. En iyi performansı elde etmek için N'yi ayarlama hem bilgisayarınıza hem de söz konusu verilere bağlıdır. Izgara ne kadar büyük olursa çalışanlar birbirlerinin işine engel olmadan o kadar çok birlikte çalışabilir.

İlginç bir nokta da bu kodun artık kapanışların yakalamaması gereken şeyleri yakalama sorununu nasıl ele aldığını belirtmektir. Şu anda kapalı yol yok. _generate ve _generateWorker işlevleri, yakalanacak çevreleri olmayan üst düzey işlevler olarak tanımlanır. Bu işlevlerin her ikisine de iletilen bağımsız değişkenler ve bu işlevlerden çıkan sonuçlar Dart kayıtları biçimindedir. Bu, compute çağrısının "bir değer girilir, bir değer çıkar" semantiğini aşmanın bir yoludur.

Artık bir ızgarada birbirine geçen kelimeleri arayarak bulmaca oluşturmak için arka plan çalışanlarından oluşan bir havuz oluşturabilirsiniz. Bu özelliği bulmaca oluşturma aracının geri kalanına sunmanın zamanı geldi.

  1. providers.dart dosyasını, workQueue sağlayıcısını aşağıdaki şekilde düzenleyerek değiştirin:

lib/providers.dart

@riverpod
Stream<model.WorkQueue> workQueue(Ref ref) async* {
  final workers = ref.watch(workerCountProvider);          // Add this line
  final size = ref.watch(sizeProvider);
  final wordListAsync = ref.watch(wordListProvider);
  final emptyCrossword = model.Crossword.crossword(
    width: size.width,
    height: size.height,
  );
  final emptyWorkQueue = model.WorkQueue.from(
    crossword: emptyCrossword,
    candidateWords: BuiltSet<String>(),
    startLocation: model.Location.at(0, 0),
  );

  ref.read(startTimeProvider.notifier).start();
  ref.read(endTimeProvider.notifier).clear();

  yield* wordListAsync.when(
    data: (wordList) => exploreCrosswordSolutions(
      crossword: emptyCrossword,
      wordList: wordList,
      maxWorkerCount: workers.count,                       // Add this line
    ),
    error: (error, stackTrace) async* {
      debugPrint('Error loading word list: $error');
      yield emptyWorkQueue;
    },
    loading: () async* {
      yield emptyWorkQueue;
    },
  );

  ref.read(endTimeProvider.notifier).end();
}
  1. Dosyanın sonuna WorkerCount sağlayıcısını aşağıdaki şekilde ekleyin:

lib/providers.dart

/// A provider that summarise the DisplayInfo from a [model.WorkQueue].
@riverpod
class DisplayInfo extends _$DisplayInfo {
  @override
  model.DisplayInfo build() => ref
      .watch(workQueueProvider)
      .when(
        data: (workQueue) => model.DisplayInfo.from(workQueue: workQueue),
        error: (error, stackTrace) => model.DisplayInfo.empty,
        loading: () => model.DisplayInfo.empty,
      );
}

enum BackgroundWorkers {                                   // Add from here
  one(1),
  two(2),
  four(4),
  eight(8),
  sixteen(16),
  thirtyTwo(32),
  sixtyFour(64),
  oneTwentyEight(128);

  const BackgroundWorkers(this.count);

  final int count;
  String get label => count.toString();
}

/// A provider that holds the current number of background workers to use.
@Riverpod(keepAlive: true)
class WorkerCount extends _$WorkerCount {
  var _count = BackgroundWorkers.four;

  @override
  BackgroundWorkers build() => _count;

  void setCount(BackgroundWorkers count) {
    _count = count;
    ref.invalidateSelf();
  }
}                                                          // To here.

Bu iki değişiklikle birlikte sağlayıcı katmanı artık arka plan yalıtılmış havuzu için maksimum çalışan sayısını, yalıtılmış işlevlerin doğru şekilde yapılandırılmasını sağlayacak şekilde ayarlama olanağı sunuyor.

  1. crossword_info_widget.dart dosyasını, CrosswordInfoWidget öğesini aşağıdaki şekilde değiştirerek güncelleyin:

lib/widgets/crossword_info_widget.dart

class CrosswordInfoWidget extends ConsumerWidget {
  const CrosswordInfoWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final size = ref.watch(sizeProvider);
    final displayInfo = ref.watch(displayInfoProvider);
    final workerCount = ref.watch(workerCountProvider).label;  // Add this line
    final startTime = ref.watch(startTimeProvider);
    final endTime = ref.watch(endTimeProvider);
    final remaining = ref.watch(expectedRemainingTimeProvider);

    return Align(
      alignment: Alignment.bottomRight,
      child: Padding(
        padding: const EdgeInsets.only(right: 32.0, bottom: 32.0),
        child: ClipRRect(
          borderRadius: BorderRadius.circular(8),
          child: ColoredBox(
            color: Theme.of(context).colorScheme.onPrimary.withAlpha(230),
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
              child: DefaultTextStyle(
                style: TextStyle(
                  fontSize: 16,
                  color: Theme.of(context).colorScheme.primary,
                ),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    _CrosswordInfoRichText(
                      label: 'Grid Size',
                      value: '${size.width} x ${size.height}',
                    ),
                    _CrosswordInfoRichText(
                      label: 'Words in grid',
                      value: displayInfo.wordsInGridCount,
                    ),
                    _CrosswordInfoRichText(
                      label: 'Candidate words',
                      value: displayInfo.candidateWordsCount,
                    ),
                    _CrosswordInfoRichText(
                      label: 'Locations to explore',
                      value: displayInfo.locationsToExploreCount,
                    ),
                    _CrosswordInfoRichText(
                      label: 'Known bad locations',
                      value: displayInfo.knownBadLocationsCount,
                    ),
                    _CrosswordInfoRichText(
                      label: 'Grid filled',
                      value: displayInfo.gridFilledPercentage,
                    ),
                    _CrosswordInfoRichText(               // Add from here
                      label: 'Max worker count',
                      value: workerCount,
                    ),                                    // To here.
                    switch ((startTime, endTime)) {
                      (null, _) => _CrosswordInfoRichText(
                        label: 'Time elapsed',
                        value: 'Not started yet',
                      ),
                      (DateTime start, null) => TickerBuilder(
                        builder: (context) => _CrosswordInfoRichText(
                          label: 'Time elapsed',
                          value: DateTime.now().difference(start).formatted,
                        ),
                      ),
                      (DateTime start, DateTime end) => _CrosswordInfoRichText(
                        label: 'Completed in',
                        value: end.difference(start).formatted,
                      ),
                    },
                    if (startTime != null && endTime == null)
                      _CrosswordInfoRichText(
                        label: 'Est. remaining',
                        value: remaining.formatted,
                      ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}
  1. crossword_generator_app.dart dosyasını, _CrosswordGeneratorMenu widget'ına aşağıdaki bölümü ekleyerek değiştirin:

lib/widgets/crossword_generator_app.dart

class _CrosswordGeneratorMenu extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) => MenuAnchor(
    menuChildren: [
      for (final entry in CrosswordSize.values)
        MenuItemButton(
          onPressed: () => ref.read(sizeProvider.notifier).setSize(entry),
          leadingIcon: entry == ref.watch(sizeProvider)
              ? Icon(Icons.radio_button_checked_outlined)
              : Icon(Icons.radio_button_unchecked_outlined),
          child: Text(entry.label),
        ),
      MenuItemButton(
        leadingIcon: ref.watch(showDisplayInfoProvider)
            ? Icon(Icons.check_box_outlined)
            : Icon(Icons.check_box_outline_blank_outlined),
        onPressed: () => ref.read(showDisplayInfoProvider.notifier).toggle(),
        child: Text('Display Info'),
      ),
      for (final count in BackgroundWorkers.values)        // Add from here
        MenuItemButton(
          leadingIcon: count == ref.watch(workerCountProvider)
              ? Icon(Icons.radio_button_checked_outlined)
              : Icon(Icons.radio_button_unchecked_outlined),
          onPressed: () =>
              ref.read(workerCountProvider.notifier).setCount(count),
          child: Text(count.label),                        // To here.
        ),
    ],
    builder: (context, controller, child) => IconButton(
      onPressed: () => controller.open(),
      icon: Icon(Icons.settings),
    ),
  );
}

Uygulamayı şimdi çalıştırırsanız bulmacaya yerleştirilecek kelimeleri aramak için oluşturulan arka plan yalıtımlarının sayısını değiştirebilirsiniz.

  1. Bulmacanın boyutunu, oluşturulan bulmacada istatistiklerin gösterilip gösterilmeyeceğini ve kullanılacak izolat sayısını içeren bağlam menüsünü açmak için dişli simgesini tıklayın.

Kelimeler ve istatistiklerin yer aldığı Çapraz Bulmaca Oluşturucu penceresi

Kontrol noktası: Çok iş parçacıklı performans

Bulmaca oluşturucuyu çalıştırmak, birden fazla çekirdeği eşzamanlı olarak kullanarak 80x44 boyutlarındaki bir bulmacanın işlem süresini önemli ölçüde kısalttı. Şunları fark edebilirsiniz:

  • Daha fazla çalışan sayısı sayesinde daha hızlı bulmaca oluşturma
  • Üretim sırasında sorunsuz kullanıcı arayüzü yanıtı
  • Oluşturma ilerleme durumunu gösteren anlık istatistikler
  • Algoritma keşif alanlarının görsel geri bildirimi

9. Oyuna dönüştürme

Geliştirdiğimiz içerik: Hazır oynanabilen bir bulmaca oyunu

Bu son bölüm aslında bir bonus turu. Kare bulmaca oluşturucuyu geliştirirken öğrendiğiniz tüm teknikleri kullanarak bir oyun oluşturacaksınız. Bu kurstan sonra:

  1. Bulmaca oluşturma: Çözülebilir bulmacalar oluşturmak için bulmaca oluşturucuyu kullanın.
  2. Kelime seçenekleri oluşturma: Her pozisyon için birden fazla kelime seçeneği sunun.
  3. Etkileşimi etkinleştirin: Kullanıcıların kelimeleri seçip yerleştirmesine izin verin.
  4. Çözümleri doğrulama: Tamamlanan bulmacanın doğru olup olmadığını kontrol edin.

Bulmaca oluşturmak için bulmaca oluşturma aracını kullanacaksınız. Kullanıcının, ızgaradaki çeşitli kelime şeklindeki boşluklara yerleştirmek üzere kelimeleri seçip seçimini kaldırmasına olanak tanımak için bağlamsal menü deyimlerini yeniden kullanacaksınız. Tüm bunların amacı, bulmacayı tamamlamaktır.

Bu oyunun kusursuz veya tamamlanmış olduğunu söylemeyeceğim. Aslında bu durumdan çok uzak. Alternatif kelime seçimini iyileştirerek çözülebilecek denge ve zorluk sorunları var. Kullanıcıları bulmacaya yönlendirecek bir eğitim yok. "Kazandınız!" ekranından bile bahsetmeyeceğim.

Bu prototip oyunu tam bir oyun haline getirmek için çok daha fazla kod yazılması gerekir. Tek bir codelab'de olması gerekenden daha fazla kod var. Bu nedenle, bu adımda, kullanılan yer ve yöntem değiştirilerek bu codelab'de şimdiye kadar öğrenilen tekniklerin pekiştirilmesi amaçlanmaktadır. Bu alıştırmanın, bu alıştırmanın önceki bölümlerinde öğrendiklerinizi pekiştireceğini umuyoruz. Alternatif olarak, bu kodu temel alarak kendi deneyimlerinizi oluşturabilirsiniz. Geliştirdiğiniz uygulamaları görmek için sabırsızlanıyoruz.

Başlamak için aşağıdaki adımları uygulayın:

  1. lib/widgets dizinindeki her şeyi silin. Oyununuz için yepyeni widget'lar oluşturacaksınız. Bu yeni widget'lar, eski widget'lardan çok şey ödünç alıyor.
  1. model.dart dosyanızı, Crossword'nin addWord yöntemini aşağıdaki gibi güncelleyecek şekilde düzenleyin:

lib/model.dart

  /// Add a word to the crossword at the given location and direction.
  Crossword? addWord({
    required Location location,
    required String word,
    required Direction direction,
    bool requireOverlap = true,                            // Add this parameter
  }) {
    // Require that the word is not already in the crossword.
    if (words.map((crosswordWord) => crosswordWord.word).contains(word)) {
      return null;
    }

    final wordCharacters = word.characters;
    bool overlap = false;

    // Check that the word fits in the crossword.
    for (final (index, character) in wordCharacters.indexed) {
      final characterLocation = switch (direction) {
        Direction.across => location.rightOffset(index),
        Direction.down => location.downOffset(index),
      };

      final target = characters[characterLocation];
      if (target != null) {
        overlap = true;
        if (target.character != character) {
          return null;
        }
        if (direction == Direction.across && target.acrossWord != null ||
            direction == Direction.down && target.downWord != null) {
          return null;
        }
      }
    }
                                                           // Edit from here
    // If overlap is required, make sure that the word overlaps with an existing
    // word. Skip this test if the crossword is empty.
    if (words.isNotEmpty && !overlap && requireOverlap) {  // To here.
      return null;
    }

    final candidate = rebuild(
      (b) => b
        ..words.add(
          CrosswordWord.word(
            word: word,
            direction: direction,
            location: location,
          ),
        ),
    );

    if (candidate.valid) {
      return candidate;
    } else {
      return null;
    }
  }

Bulmaca modelinizde yapılan bu küçük değişiklik, çakışmayan kelimelerin eklenmesini sağlar. Oyuncuların tahtanın herhangi bir yerinde oynamasına izin vermek ve yine de oyuncunun hamlelerini saklamak için temel model olarak Crossword kullanabilmek faydalıdır. Bu, yalnızca belirli konumlardaki kelimelerin belirli bir yönde yerleştirildiği bir listedir.

  1. CrosswordPuzzleGame model sınıfını model.dart dosyanızın sonuna ekleyin.

lib/model.dart

/// Creates a puzzle from a crossword and a set of candidate words.
abstract class CrosswordPuzzleGame
    implements Built<CrosswordPuzzleGame, CrosswordPuzzleGameBuilder> {
  static Serializer<CrosswordPuzzleGame> get serializer =>
      _$crosswordPuzzleGameSerializer;

  /// The [Crossword] that this puzzle is based on.
  Crossword get crossword;

  /// The alternate words for each [CrosswordWord] in the crossword.
  BuiltMap<Location, BuiltMap<Direction, BuiltList<String>>> get alternateWords;

  /// The player's selected words.
  BuiltList<CrosswordWord> get selectedWords;

  bool canSelectWord({
    required Location location,
    required String word,
    required Direction direction,
  }) {
    final crosswordWord = CrosswordWord.word(
      word: word,
      location: location,
      direction: direction,
    );

    if (selectedWords.contains(crosswordWord)) {
      return true;
    }

    var puzzle = this;

    if (puzzle.selectedWords
        .where((b) => b.direction == direction && b.location == location)
        .isNotEmpty) {
      puzzle = puzzle.rebuild(
        (b) => b
          ..selectedWords.removeWhere(
            (selectedWord) =>
                selectedWord.location == location &&
                selectedWord.direction == direction,
          ),
      );
    }

    return null !=
        puzzle.crosswordFromSelectedWords.addWord(
          location: location,
          word: word,
          direction: direction,
          requireOverlap: false,
        );
  }

  CrosswordPuzzleGame? selectWord({
    required Location location,
    required String word,
    required Direction direction,
  }) {
    final crosswordWord = CrosswordWord.word(
      word: word,
      location: location,
      direction: direction,
    );

    if (selectedWords.contains(crosswordWord)) {
      return rebuild((b) => b.selectedWords.remove(crosswordWord));
    }

    var puzzle = this;

    if (puzzle.selectedWords
        .where((b) => b.direction == direction && b.location == location)
        .isNotEmpty) {
      puzzle = puzzle.rebuild(
        (b) => b
          ..selectedWords.removeWhere(
            (selectedWord) =>
                selectedWord.location == location &&
                selectedWord.direction == direction,
          ),
      );
    }

    // Check if the selected word meshes with the already selected words.
    // Note this version of the crossword does not enforce overlap to
    // allow the player to select words anywhere on the grid. Enforcing words
    // to be solved in order is a possible alternative.
    final updatedSelectedWordsCrossword = puzzle.crosswordFromSelectedWords
        .addWord(
          location: location,
          word: word,
          direction: direction,
          requireOverlap: false,
        );

    // Make sure the selected word is in the crossword or is an alternate word.
    if (updatedSelectedWordsCrossword != null) {
      if (puzzle.crossword.words.contains(crosswordWord) ||
          puzzle.alternateWords[location]?[direction]?.contains(word) == true) {
        return puzzle.rebuild(
          (b) => b
            ..selectedWords.add(
              CrosswordWord.word(
                word: word,
                location: location,
                direction: direction,
              ),
            ),
        );
      }
    }
    return null;
  }

  /// The crossword from the selected words.
  Crossword get crosswordFromSelectedWords => Crossword.crossword(
    width: crossword.width,
    height: crossword.height,
    words: selectedWords,
  );

  /// Test if the puzzle is solved. Note, this allows for the possibility of
  /// multiple solutions.
  bool get solved =>
      crosswordFromSelectedWords.valid &&
      crosswordFromSelectedWords.words.length == crossword.words.length &&
      crossword.words.isNotEmpty;

  /// Create a crossword puzzle game from a crossword and a set of candidate
  /// words.
  factory CrosswordPuzzleGame.from({
    required Crossword crossword,
    required BuiltSet<String> candidateWords,
  }) {
    // Remove all of the currently used words from the list of candidates
    candidateWords = candidateWords.rebuild(
      (p0) => p0.removeAll(crossword.words.map((p1) => p1.word)),
    );

    // This is the list of alternate words for each word in the crossword
    var alternates =
        BuiltMap<Location, BuiltMap<Direction, BuiltList<String>>>();

    // Build the alternate words for each word in the crossword
    for (final crosswordWord in crossword.words) {
      final alternateWords = candidateWords.toBuiltList().rebuild(
        (b) => b
          ..where((b) => b.length == crosswordWord.word.length)
          ..shuffle()
          ..take(4)
          ..sort(),
      );

      candidateWords = candidateWords.rebuild(
        (b) => b.removeAll(alternateWords),
      );

      alternates = alternates.rebuild(
        (b) => b.updateValue(
          crosswordWord.location,
          (b) => b.rebuild(
            (b) => b.updateValue(
              crosswordWord.direction,
              (b) => b.rebuild((b) => b.replace(alternateWords)),
              ifAbsent: () => alternateWords,
            ),
          ),
          ifAbsent: () => {crosswordWord.direction: alternateWords}.build(),
        ),
      );
    }

    return CrosswordPuzzleGame((b) {
      b
        ..crossword.replace(crossword)
        ..alternateWords.replace(alternates);
    });
  }

  factory CrosswordPuzzleGame([
    void Function(CrosswordPuzzleGameBuilder)? updates,
  ]) = _$CrosswordPuzzleGame;
  CrosswordPuzzleGame._();
}

/// Construct the serialization/deserialization code for the data model.
@SerializersFor([
  Location,
  Crossword,
  CrosswordWord,
  CrosswordCharacter,
  WorkQueue,
  DisplayInfo,
  CrosswordPuzzleGame,                                     // Add this line
])
final Serializers serializers = _$serializers;

providers.dart dosyasındaki güncellemeler, ilginç bir değişiklik karışımıdır. İstatistik toplama işlemini desteklemek için kullanılan sağlayıcıların çoğu kaldırıldı. Arka plan yalıtımlarının sayısını değiştirme özelliği kaldırıldı ve sabit bir değerle değiştirildi. Ayrıca, daha önce eklediğiniz yeni CrosswordPuzzleGame modeline erişim sağlayan yeni bir sağlayıcı da vardır.

lib/providers.dart

import 'dart:convert';
                                                           // Drop the dart:math import

import 'package:built_collection/built_collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'isolates.dart';
import 'model.dart' as model;

part 'providers.g.dart';

const backgroundWorkerCount = 4;                           // Add this line

/// A provider for the wordlist to use when generating the crossword.
@riverpod
Future<BuiltSet<String>> wordList(Ref ref) async {
  // This codebase requires that all words consist of lowercase characters
  // in the range 'a'-'z'. Words containing uppercase letters will be
  // lowercased, and words containing runes outside this range will
  // be removed.

  final re = RegExp(r'^[a-z]+$');
  final words = await rootBundle.loadString('assets/words.txt');
  return const LineSplitter()
      .convert(words)
      .toBuiltSet()
      .rebuild(
        (b) => b
          ..map((word) => word.toLowerCase().trim())
          ..where((word) => word.length > 2)
          ..where((word) => re.hasMatch(word)),
      );
}

/// An enumeration for different sizes of [model.Crossword]s.
enum CrosswordSize {
  small(width: 20, height: 11),
  medium(width: 40, height: 22),
  large(width: 80, height: 44),
  xlarge(width: 160, height: 88),
  xxlarge(width: 500, height: 500);

  const CrosswordSize({required this.width, required this.height});

  final int width;
  final int height;
  String get label => '$width x $height';
}

/// A provider that holds the current size of the crossword to generate.
@Riverpod(keepAlive: true)
class Size extends _$Size {
  var _size = CrosswordSize.medium;

  @override
  CrosswordSize build() => _size;

  void setSize(CrosswordSize size) {
    _size = size;
    ref.invalidateSelf();
  }
}

@riverpod
Stream<model.WorkQueue> workQueue(Ref ref) async* {
  final size = ref.watch(sizeProvider);                   // Drop the ref.watch(workerCountProvider)
  final wordListAsync = ref.watch(wordListProvider);
  final emptyCrossword = model.Crossword.crossword(
    width: size.width,
    height: size.height,
  );
  final emptyWorkQueue = model.WorkQueue.from(
    crossword: emptyCrossword,
    candidateWords: BuiltSet<String>(),
    startLocation: model.Location.at(0, 0),
  );
                                                          // Drop the startTimeProvider and endTimeProvider refs
  yield* wordListAsync.when(
    data: (wordList) => exploreCrosswordSolutions(
      crossword: emptyCrossword,
      wordList: wordList,
      maxWorkerCount: backgroundWorkerCount,              // Edit this line
    ),
    error: (error, stackTrace) async* {
      debugPrint('Error loading word list: $error');
      yield emptyWorkQueue;
    },
    loading: () async* {
      yield emptyWorkQueue;
    },
  );
}                                                         // Drop the endTimeProvider ref

@riverpod                                                 // Add from here to end of file
class Puzzle extends _$Puzzle {
  model.CrosswordPuzzleGame _puzzle = model.CrosswordPuzzleGame.from(
    crossword: model.Crossword.crossword(width: 0, height: 0),
    candidateWords: BuiltSet<String>(),
  );

  @override
  model.CrosswordPuzzleGame build() {
    final size = ref.watch(sizeProvider);
    final wordList = ref.watch(wordListProvider).value;
    final workQueue = ref.watch(workQueueProvider).value;

    if (wordList != null &&
        workQueue != null &&
        workQueue.isCompleted &&
        (_puzzle.crossword.height != size.height ||
            _puzzle.crossword.width != size.width ||
            _puzzle.crossword != workQueue.crossword)) {
      compute(_puzzleFromCrosswordTrampoline, (
        workQueue.crossword,
        wordList,
      )).then((puzzle) {
        _puzzle = puzzle;
        ref.invalidateSelf();
      });
    }

    return _puzzle;
  }

  Future<void> selectWord({
    required model.Location location,
    required String word,
    required model.Direction direction,
  }) async {
    final candidate = await compute(_puzzleSelectWordTrampoline, (
      _puzzle,
      location,
      word,
      direction,
    ));

    if (candidate != null) {
      _puzzle = candidate;
      ref.invalidateSelf();
    } else {
      debugPrint('Invalid word selection: $word');
    }
  }

  bool canSelectWord({
    required model.Location location,
    required String word,
    required model.Direction direction,
  }) {
    return _puzzle.canSelectWord(
      location: location,
      word: word,
      direction: direction,
    );
  }
}

// Trampoline functions to disentangle these Isolate target calls from the
// unsendable reference to the [Puzzle] provider.

Future<model.CrosswordPuzzleGame> _puzzleFromCrosswordTrampoline(
  (model.Crossword, BuiltSet<String>) args,
) async =>
    model.CrosswordPuzzleGame.from(crossword: args.$1, candidateWords: args.$2);

model.CrosswordPuzzleGame? _puzzleSelectWordTrampoline(
  (model.CrosswordPuzzleGame, model.Location, String, model.Direction) args,
) => args.$1.selectWord(location: args.$2, word: args.$3, direction: args.$4);

Puzzle sağlayıcının en ilginç kısımları, Crossword ve wordList öğelerinden CrosswordPuzzleGame oluşturmanın maliyetini ve kelime seçme maliyetini örtbas etmek için kullanılan stratejilerdir. Bu işlemlerin her ikisi de arka planı yalıtma özelliği kullanılmadan yapıldığında kullanıcı arayüzü etkileşiminin yavaşlamasına neden olur. Sonuç arka planda hesaplanırken ara sonucu göstermek için el çabukluğuyla yapılan bazı işlemler sayesinde, gerekli hesaplamalar arka planda yapılırken duyarlı bir kullanıcı arayüzü elde edersiniz.

  1. Artık boş olan lib/widgets dizininde aşağıdaki içeriğe sahip bir crossword_puzzle_app.dart dosyası oluşturun:

lib/widgets/crossword_puzzle_app.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../providers.dart';
import 'crossword_generator_widget.dart';
import 'crossword_puzzle_widget.dart';
import 'puzzle_completed_widget.dart';

class CrosswordPuzzleApp extends StatelessWidget {
  const CrosswordPuzzleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return _EagerInitialization(
      child: Scaffold(
        appBar: AppBar(
          actions: [_CrosswordPuzzleAppMenu()],
          titleTextStyle: TextStyle(
            color: Theme.of(context).colorScheme.primary,
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
          title: Text('Crossword Puzzle'),
        ),
        body: SafeArea(
          child: Consumer(
            builder: (context, ref, _) {
              final workQueueAsync = ref.watch(workQueueProvider);
              final puzzleSolved = ref.watch(
                puzzleProvider.select((puzzle) => puzzle.solved),
              );

              return workQueueAsync.when(
                data: (workQueue) {
                  if (puzzleSolved) {
                    return PuzzleCompletedWidget();
                  }
                  if (workQueue.isCompleted &&
                      workQueue.crossword.characters.isNotEmpty) {
                    return CrosswordPuzzleWidget();
                  }
                  return CrosswordGeneratorWidget();
                },
                loading: () => Center(child: CircularProgressIndicator()),
                error: (error, stackTrace) => Center(child: Text('$error')),
              );
            },
          ),
        ),
      ),
    );
  }
}

class _EagerInitialization extends ConsumerWidget {
  const _EagerInitialization({required this.child});
  final Widget child;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.watch(wordListProvider);
    return child;
  }
}

class _CrosswordPuzzleAppMenu extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) => MenuAnchor(
    menuChildren: [
      for (final entry in CrosswordSize.values)
        MenuItemButton(
          onPressed: () => ref.read(sizeProvider.notifier).setSize(entry),
          leadingIcon: entry == ref.watch(sizeProvider)
              ? Icon(Icons.radio_button_checked_outlined)
              : Icon(Icons.radio_button_unchecked_outlined),
          child: Text(entry.label),
        ),
    ],
    builder: (context, controller, child) => IconButton(
      onPressed: () => controller.open(),
      icon: Icon(Icons.settings),
    ),
  );
}

Bu dosyanın çoğu, artık oldukça tanıdık olmalıdır. Evet, tanımlanmamış widget'lar olacak. Bunları düzeltmeye başlayabilirsiniz.

  1. Bir crossword_generator_widget.dart dosyası oluşturup aşağıdaki içeriği ekleyin:

lib/widgets/crossword_generator_widget.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart';

import '../model.dart';
import '../providers.dart';

class CrosswordGeneratorWidget extends ConsumerWidget {
  const CrosswordGeneratorWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final size = ref.watch(sizeProvider);
    return TableView.builder(
      diagonalDragBehavior: DiagonalDragBehavior.free,
      cellBuilder: _buildCell,
      columnCount: size.width,
      columnBuilder: (index) => _buildSpan(context, index),
      rowCount: size.height,
      rowBuilder: (index) => _buildSpan(context, index),
    );
  }

  TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) {
    final location = Location.at(vicinity.column, vicinity.row);

    return TableViewCell(
      child: Consumer(
        builder: (context, ref, _) {
          final character = ref.watch(
            workQueueProvider.select(
              (workQueueAsync) => workQueueAsync.when(
                data: (workQueue) => workQueue.crossword.characters[location],
                error: (error, stackTrace) => null,
                loading: () => null,
              ),
            ),
          );

          final explorationCell = ref.watch(
            workQueueProvider.select(
              (workQueueAsync) => workQueueAsync.when(
                data: (workQueue) =>
                    workQueue.locationsToTry.keys.contains(location),
                error: (error, stackTrace) => false,
                loading: () => false,
              ),
            ),
          );

          if (character != null) {
            return AnimatedContainer(
              duration: Durations.extralong1,
              curve: Curves.easeInOut,
              color: explorationCell
                  ? Theme.of(context).colorScheme.primary
                  : Theme.of(context).colorScheme.onPrimary,
              child: Center(
                child: AnimatedDefaultTextStyle(
                  duration: Durations.extralong1,
                  curve: Curves.easeInOut,
                  style: TextStyle(
                    fontSize: 24,
                    color: explorationCell
                        ? Theme.of(context).colorScheme.onPrimary
                        : Theme.of(context).colorScheme.primary,
                  ),
                  child: Text('•'), // https://www.compart.com/en/unicode/U+2022
                ),
              ),
            );
          }

          return ColoredBox(
            color: Theme.of(context).colorScheme.primaryContainer,
          );
        },
      ),
    );
  }

  TableSpan _buildSpan(BuildContext context, int index) {
    return TableSpan(
      extent: FixedTableSpanExtent(32),
      foregroundDecoration: TableSpanDecoration(
        border: TableSpanBorder(
          leading: BorderSide(
            color: Theme.of(context).colorScheme.onPrimaryContainer,
          ),
          trailing: BorderSide(
            color: Theme.of(context).colorScheme.onPrimaryContainer,
          ),
        ),
      ),
    );
  }
}

Bu da makul ölçüde bilinen bir şey olmalıdır. Temel fark, oluşturulan kelimelerin karakterlerini göstermek yerine artık bilinmeyen bir karakterin varlığını belirtmek için bir Unicode karakteri göstermenizdir. Bu gerçekten de estetiği iyileştirmek için biraz çalışmaya ihtiyaç duyuyor.

  1. crossword_puzzle_widget.dart dosyası oluşturun ve aşağıdaki içeriği ekleyin:

lib/widgets/crossword_puzzle_widget.dart

import 'package:built_collection/built_collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart';

import '../model.dart';
import '../providers.dart';

class CrosswordPuzzleWidget extends ConsumerWidget {
  const CrosswordPuzzleWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final size = ref.watch(sizeProvider);
    return TableView.builder(
      diagonalDragBehavior: DiagonalDragBehavior.free,
      cellBuilder: _buildCell,
      columnCount: size.width,
      columnBuilder: (index) => _buildSpan(context, index),
      rowCount: size.height,
      rowBuilder: (index) => _buildSpan(context, index),
    );
  }

  TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) {
    final location = Location.at(vicinity.column, vicinity.row);

    return TableViewCell(
      child: Consumer(
        builder: (context, ref, _) {
          final character = ref.watch(
            puzzleProvider.select(
              (puzzle) => puzzle.crossword.characters[location],
            ),
          );
          final selectedCharacter = ref.watch(
            puzzleProvider.select(
              (puzzle) =>
                  puzzle.crosswordFromSelectedWords.characters[location],
            ),
          );
          final alternateWords = ref.watch(
            puzzleProvider.select((puzzle) => puzzle.alternateWords),
          );

          if (character != null) {
            final acrossWord = character.acrossWord;
            var acrossWords = BuiltList<String>();
            if (acrossWord != null) {
              acrossWords = acrossWords.rebuild(
                (b) => b
                  ..add(acrossWord.word)
                  ..addAll(
                    alternateWords[acrossWord.location]?[acrossWord
                            .direction] ??
                        [],
                  )
                  ..sort(),
              );
            }

            final downWord = character.downWord;
            var downWords = BuiltList<String>();
            if (downWord != null) {
              downWords = downWords.rebuild(
                (b) => b
                  ..add(downWord.word)
                  ..addAll(
                    alternateWords[downWord.location]?[downWord.direction] ??
                        [],
                  )
                  ..sort(),
              );
            }

            return MenuAnchor(
              builder: (context, controller, _) {
                return GestureDetector(
                  onTapDown: (details) =>
                      controller.open(position: details.localPosition),
                  child: AnimatedContainer(
                    duration: Durations.extralong1,
                    curve: Curves.easeInOut,
                    color: Theme.of(context).colorScheme.onPrimary,
                    child: Center(
                      child: AnimatedDefaultTextStyle(
                        duration: Durations.extralong1,
                        curve: Curves.easeInOut,
                        style: TextStyle(
                          fontSize: 24,
                          color: Theme.of(context).colorScheme.primary,
                        ),
                        child: Text(selectedCharacter?.character ?? ''),
                      ),
                    ),
                  ),
                );
              },
              menuChildren: [
                if (acrossWords.isNotEmpty && downWords.isNotEmpty)
                  Padding(
                    padding: const EdgeInsets.all(4),
                    child: Text('Across'),
                  ),
                for (final word in acrossWords)
                  _WordSelectMenuItem(
                    location: acrossWord!.location,
                    word: word,
                    selectedCharacter: selectedCharacter,
                    direction: Direction.across,
                  ),
                if (acrossWords.isNotEmpty && downWords.isNotEmpty)
                  Padding(
                    padding: const EdgeInsets.all(4),
                    child: Text('Down'),
                  ),
                for (final word in downWords)
                  _WordSelectMenuItem(
                    location: downWord!.location,
                    word: word,
                    selectedCharacter: selectedCharacter,
                    direction: Direction.down,
                  ),
              ],
            );
          }

          return ColoredBox(
            color: Theme.of(context).colorScheme.primaryContainer,
          );
        },
      ),
    );
  }

  TableSpan _buildSpan(BuildContext context, int index) {
    return TableSpan(
      extent: FixedTableSpanExtent(32),
      foregroundDecoration: TableSpanDecoration(
        border: TableSpanBorder(
          leading: BorderSide(
            color: Theme.of(context).colorScheme.onPrimaryContainer,
          ),
          trailing: BorderSide(
            color: Theme.of(context).colorScheme.onPrimaryContainer,
          ),
        ),
      ),
    );
  }
}

class _WordSelectMenuItem extends ConsumerWidget {
  const _WordSelectMenuItem({
    required this.location,
    required this.word,
    required this.selectedCharacter,
    required this.direction,
  });

  final Location location;
  final String word;
  final CrosswordCharacter? selectedCharacter;
  final Direction direction;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final notifier = ref.read(puzzleProvider.notifier);
    return MenuItemButton(
      onPressed:
          ref.watch(
            puzzleProvider.select(
              (puzzle) => puzzle.canSelectWord(
                location: location,
                word: word,
                direction: direction,
              ),
            ),
          )
          ? () => notifier.selectWord(
              location: location,
              word: word,
              direction: direction,
            )
          : null,
      leadingIcon:
          switch (direction) {
            Direction.across => selectedCharacter?.acrossWord?.word == word,
            Direction.down => selectedCharacter?.downWord?.word == word,
          }
          ? Icon(Icons.radio_button_checked_outlined)
          : Icon(Icons.radio_button_unchecked_outlined),
      child: Text(word),
    );
  }
}

Bu widget, geçmişte başka yerlerde kullanıldığını gördüğünüz parçalardan oluşturulmuş olsa da bir öncekinden biraz daha yoğun. Artık her doldurulmuş hücre tıklandığında, kullanıcının seçebileceği kelimelerin listelendiği bir bağlam menüsü oluşturuyor. Kelime seçildiyse çakışan kelimeler seçilemez. Kullanıcı, bir kelimenin seçimini kaldırmak için o kelimenin menü öğesine dokunur.

Oyuncunun tüm bulmacayı doldurmak için kelimeler seçebildiğini varsayarsak "Kazandınız!" ekranına ihtiyacınız var.

  1. Bir puzzle_completed_widget.dart dosyası oluşturup aşağıdaki içeriği ekleyin:

lib/widgets/puzzle_completed_widget.dart

import 'package:flutter/material.dart';

class PuzzleCompletedWidget extends StatelessWidget {
  const PuzzleCompletedWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        'Puzzle Completed!',
        style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold),
      ),
    );
  }
}

Bu bilgiyi daha da ilginç hâle getirebileceğinizden eminim. Animasyon araçları hakkında daha fazla bilgi edinmek için Flutter'da yeni nesil kullanıcı arayüzleri oluşturma başlıklı codelab'i inceleyin.

  1. lib/main.dart dosyanızı aşağıdaki gibi düzenleyin:

lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'widgets/crossword_puzzle_app.dart';                 // Update this line

void main() {
  runApp(
    ProviderScope(
      child: MaterialApp(
        title: 'Crossword Puzzle',                          // Update this line
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          colorSchemeSeed: Colors.blueGrey,
          brightness: Brightness.light,
        ),
        home: CrosswordPuzzleApp(),                         // Update this line
      ),
    ),
  );
}

Bu uygulamayı çalıştırdığınızda, bulmaca oluşturucu bulmacanızı oluştururken animasyonu görürsünüz. Ardından, çözmeniz için boş bir bulmaca gösterilir. Sorunu çözdüğünüzde aşağıdaki gibi bir ekranla karşılaşırsınız:

&quot;Bulmaca tamamlandı!&quot; metnini gösteren Çengel Bulmaca uygulaması penceresi

10. Tebrikler

Tebrikler! Flutter ile bulmaca oyunu oluşturmayı başardınız.

Bulmaca oyununa dönüşen bir bulmaca oluşturucu geliştirdiniz. İzole edilmiş bir havuzda arka plan hesaplamaları yapma konusunda uzmanlaştınız. Geri izleme algoritmasının uygulanmasını kolaylaştırmak için değişmez veri yapıları kullandınız. Ayrıca, bir sonraki tablo verilerini görüntülemeniz gerektiğinde işinize yarayacak olan TableView ile kaliteli zaman geçirdiniz.

Daha fazla bilgi