Flutter ile kelime bulmacası oluşturun

1. Başlamadan önce

Dünyanın en büyük çapraz bulmacasını oluşturmanın mümkün olup olmadığının sorulduğunu hayal edin. Okulda öğrendiğiniz bazı yapay zeka tekniklerini hatırlıyorsunuz ve yoğun hesaplama gerektiren problemlere çözüm üretmek için algoritmik seçenekleri keşfetmek amacıyla Flutter'ı kullanıp kullanamayacağınızı merak ediyorsunuz.

Bu codelab'de tam olarak bunu yapacaksınız. Laboratuvarın sonunda kelime ızgarası bulmacaları oluşturmaya yönelik algoritmalar alanında oynayabileceğiniz bir araç geliştiriyorsunuz. Geçerli bir çapraz bulmacanın ne olduğunun birçok farklı tanımı vardır ve bu teknikler tanımınıza uygun bulmacalar oluşturmanıza yardımcı olur.

Oluşturulan bir çapraz bulmacanın animasyonu.

Bu aracı temel alarak, kullanıcıların çözeceği bulmacayı oluşturmak için çapraz kelime oluşturucuyu kullanan bir çapraz bulmaca oluşturursunuz. Bu bulmaca Android, iOS, Windows, macOS ve Linux'ta kullanılabilir. Android'de burada:

Pixel Fold emülatöründe çözülen çapraz bulmacanın ekran görüntüsü.

Ön koşullar

Öğrenecekleriniz

  • İzoleler, Flutter'ın oluşturma döngüsünü, Flutter'ın compute işlevi ve Riverpod'un select yeniden oluşturma filtresinin değer önbelleğe alma özelliklerinin bir kombinasyonuyla bozmadan işlem yükü açısından pahalı işler yapmak için nasıl kullanılır?
  • Derinlik öncelikli arama ve geri izleme gibi arama tabanlı İyi Eski Moda Yapay Zeka (GOFAI) tekniklerinin uygulanmasını kolaylaştırmak için built_value ve built_collection ile sabit 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'sı.
  • Flutter ve Dart eklentileriyle Visual Studio Kodu (VS Code).
  • Seçtiğiniz geliştirme hedefi için derleyici yazılımı. Bu codelab tüm masaüstü platformlarda, Android ve iOS'te kullanılabilir. Windows'u hedeflemek için VS Kodu, macOS veya iOS'i hedeflemek için Xcode, Android'i hedeflemek için Android Studio'ya ihtiyacınız vardır.

2. Proje oluşturma

İlk Flutter projenizi oluşturun

  1. VS Code'u başlatın.
  2. Komut satırına flutter new yazın ve ardından menüden Flutter: New Project'i seçin.

VS Code'un ekran görüntüsü:

  1. Boş uygulama'yı seçin ve ardından projenizin oluşturulacağı dizini seçin. Bu dizin, üst düzey ayrıcalıklar gerektirmeyen veya yolunda boşluk bulunan herhangi bir dizin olmalıdır. Buna örnek olarak ana dizininiz veya C:\src\ verilebilir.

Yeni uygulama akışının parçası olarak seçili olarak gösterilen Boş Uygulama içeren VS Kodunun ekran görüntüsü

  1. Projenize generate_crossword adını verin. Bu codelab'in geri kalanında uygulamanıza generate_crossword adını verdiğiniz varsayılmaktadır.

VS Code'un ekran görüntüsü

Flutter artık proje klasörünüzü oluşturur ve VS Code bu klasörü açar. Şimdi uygulamanın temel bir iskeletini kullanarak iki dosyanın içeriğinin üzerine yazacaksınız.

İlk uygulamayı kopyalayıp yapıştırın

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

VS Code'un kısmi ekran görüntüsü ve pubspec.yaml dosyasının konumunu vurgulayan oklar gösteriliyor.

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

pubspec.yaml

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

environment:
  sdk: '>=3.3.3 <4.0.0'

dependencies:
  built_collection: ^5.1.1
  built_value: ^8.9.2
  characters: ^1.3.0
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.5.1
  intl: ^0.19.0
  riverpod: ^2.5.1
  riverpod_annotation: ^2.3.5
  two_dimensional_scrollables: ^0.2.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0
  build_runner: ^2.4.9
  built_value_generator: ^8.9.2
  custom_lint: ^0.6.4
  riverpod_generator: ^2.4.0
  riverpod_lint: ^2.3.10

flutter:
  uses-material-design: true

pubspec.yaml dosyası, uygulamanızın mevcut sürümü ve bağımlılıkları gibi temel bilgileri belirtir. Normal boş bir Flutter uygulamasının parçası olmayan bir bağımlılık koleksiyonu görürsünüz. Gelecek adımlarda tüm bu paketlerden yararlanacaksınız.

  1. main.dart dosyasını lib/ dizininde açın.

Main.dart dosyasının konumunu gösteren bir okla birlikte VS Code&#39;un kısmi ekran görüntüsü

  1. Bu dosyanın içeriğini şununla 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(
          useMaterial3: true,
          colorSchemeSeed: Colors.blueGrey,
          brightness: Brightness.light,
        ),
        home: Scaffold(
          body: Center(
            child: Text(
              'Hello, World!',
              style: TextStyle(fontSize: 24),
            ),
          ),
        ),
      ),
    ),
  );
}
  1. Her şeyin çalışıp çalışmadığını kontrol etmek için bu kodu çalıştırın. Her yeni projenin zorunlu başlangıç ifadesini içeren yeni bir pencere görüntülenmelidir. Bu uygulamanın, eyalet yönetimi için riverpod kullanacağını gösteren bir ProviderScope vardır.

Üzerinde &quot;Hello, World!&quot; ifadesinin yer aldığı bir uygulama penceresi ortada

3. Kelime ekleme

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

Bulmacanın kalbinde, bir dizi kelime bulunur. Kelimeler, bazıları boyunca, bazıları aşağı doğru olacak şekilde bir ızgarada düzenlenir. Tek bir kelimeyi çözmek bu ilk kelimeyle kesişen kelimelerle ilgili ipuçları verir. Dolayısıyla, ilk yapı taşı bir kelime listesi olmalıdır.

Peter Norvig'in Doğal Dil Topluluk Verileri sayfası bu kelimeler için iyi bir kaynaktır. 267.750 kelimeden oluşan SOWPODS listesi, faydalı bir başlangıç noktası olarak kullanılabilir.

Bu adımda bir kelime listesi indirecek, bunu Flutter uygulamanıza bir öğe olarak ekleyecek ve bir Riverpod sağlayıcısını başlangıçta listeyi uygulamaya yükleyecek şekilde düzenleyeceksiniz.

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

  1. Projenizin pubspec.yaml dosyasını değiştirerek seçtiğiniz kelime listesine aşağıdaki öğe bildirimini ekleyin. Geri kalan her şey aynı kaldığı için bu girişte yalnızca uygulamanızın yapılandırmasındaki değişimli cümle gösterilmektedir.

pubspec.yaml

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

Bu dosyayı henüz oluşturmadığınız için düzenleyiciniz muhtemelen bir uyarıyla bu son satırı vurgular.

  1. Tarayıcınızı ve düzenleyicinizi kullanarak projenizin üst düzeyinde bir assets dizini oluşturun ve bu dizinde, yukarıda bağlantısı verilen kelime listelerinden birini kullanarak bir words.txt dosyası oluşturun.

Bu kod, yukarıda bahsedilen SOWPODS listesi kullanılarak tasarlanmıştır, ancak yalnızca A-Z karakterlerinden oluşan kelime listeleriyle kullanılabilir. Bu kod tabanını farklı karakter kümeleriyle çalışacak şekilde genişletmek, okuyucuya alıştırma olarak bırakılır.

Kelimeleri yükle

Uygulama başlatılırken kelime listesinin yüklenmesinden sorumlu kodu yazmak için şu adımları izleyin:

  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_annotation/riverpod_annotation.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)));
}

Bu kod tabanı için ilk Riverpod sağlayıcınız oldu. Düzenleyicinizin tanımlanmamış sınıf veya oluşturulmamış hedef olarak bildireceği çeşitli alanlar olduğunu fark edeceksiniz. Bu projede, Riverpod dahil olmak üzere birden fazla bağımlılık için kod oluşturma kullanıldığından tanımlanmamış sınıf hataları beklenmektedir.

  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 eder ve siz projede değişiklik yaptıkça oluşturulan dosyalar güncellenir. Bu komut providers.g.dart içinde kodu oluşturduktan sonra, düzenleyiciniz yukarıda providers.dart bölümüne eklediğiniz koddan memnun kalacaktır.

Riverpod'da, yukarıda tanımladığınız wordList işlevi gibi sağlayıcılar genellikle geç örneklendirilir. Ancak, bu uygulamanın amaçlarınız doğrultusunda kelime listesinin her zaman hızlı bir şekilde yüklenmesi gerekir. Riverpod belgelerinde, yoğun bir şekilde yüklemeniz gereken sağlayıcılarla çalışırken aşağıdaki yaklaşım öneriliyor. Bunu hemen uygulayacaksınız.

  1. lib/widgets dizininde bir crossword_generator_app.dart 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 ilginç. Bunların ilki _EagerInitialization widget'ıdır. Bunların tek görevi yukarıda oluşturduğunuz wordList sağlayıcısının kelime listesini yüklemesini zorunlu tutmaktır. Bu widget, sağlayıcıyı ref.watch() çağrısını kullanarak dinleyerek bu hedefe ulaşır. Sağlayıcıların kolayca başlatılması başlıklı Riverpod dokümanlarında bu teknik hakkında daha fazla bilgi edinebilirsiniz.

Bu dosyada dikkat edilmesi gereken ikinci ilginç nokta, Riverpod'un eşzamansız içeriği nasıl ele aldığıdır. Hatırlayacağınız üzere, diskten içerik yükleme yavaş olduğundan wordList sağlayıcısı eşzamansız bir işlev olarak tanımlanmıştır. Bu koddaki kelime listesi sağlayıcısını izlerken bir AsyncValue<BuiltSet<String>> alırsınız. Bu türün AsyncValue bölümü, 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 öğesinin when yöntemi, gelecekteki değerin içinde bulunabileceği üç olası durumu işler. Gelecek başarıyla çözülmüş olabilir. Bu durumda, data geri çağırması çağrılır ve bir hata durumunda olabilir. Bu durumda, error geri çağırması çağrılır veya son olarak hâlâ yükleniyor olabilir. Geri çağırmanın sonucu, when yöntemi tarafından döndürüldüğünden, üç geri çağırmanın dönüş türlerinin uyumlu döndürme türleri olmalıdır. Bu örnekte, zaman yönteminin sonucu, Scaffold widget'ının body öğesi olarak gösterilir.

Neredeyse sonsuz sayıda liste uygulaması oluşturun

CrosswordGeneratorApp widget'ını uygulamanıza entegre etmek için şu adımları izleyin:

  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(
          useMaterial3: true,
          colorSchemeSeed: Colors.blueGrey,
          brightness: Brightness.light,
        ),
        home: CrosswordGeneratorApp(),                     // Remove what was here and replace
      ),
    ),
  );
}
  1. Uygulamayı yeniden başlatın. Neredeyse sonsuza kadar sürecek bir kaydırma listesi göreceksiniz.

&quot;Çapraz Kelime Oluşturucu&quot; başlıklı bir uygulama penceresi ve bir kelime listesi

4. Kelimeleri bir ızgarada göster

Bu adımda, built_value ve built_collection paketlerini kullanarak çapraz bulmaca oluşturmak için bir veri yapısı oluşturacaksınız. Bu iki paket, veri yapılarının sabit değerler olarak oluşturulmasına olanak tanır. Bu da hem yalıtımlar arasında veri aktarımı hem de derinlik ilk önce arama ile geri izlemeyi çok daha kolay bir şekilde gerçekleştirme açısından faydalıdır.

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

  1. lib dizininde bir model.dart dosyası oluşturun ve aşağıdaki içeriği dosyaya 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, bulmacalar oluşturmak için kullanacağınız veri yapısının başlangıcını açıklar. Çapraz bulmacanın özünde, bir ızgarada iç içe geçmiş yatay ve dikey kelimelerden oluşan bir liste bulunur. Bu veri yapısını kullanmak için adlandırılmış Crossword.crossword oluşturucuyla uygun boyutta bir Crossword oluşturur, ardından addWord yöntemini kullanarak kelimeleri eklersiniz. Kesinleşmiş değeri oluşturma sürecinin bir parçası olarak _fillCharacters yöntemi ile CrosswordCharacter içeren bir tablo oluşturulur.

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

  1. lib dizininde bir utils dosyası oluşturun ve aşağıdaki içeriği dosyaya 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, kümenin rastgele bir öğesini almayı kolaylaştıran BuiltSet uzantısıdır. Uzantı yöntemleri, ek işlevlerle sınıfları genişletmeyi kolaylaştırır. Uzantının utils.dart dosyasının dışında kullanılabilmesi için uzantıya ad verilmesi 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, yukarıda tanımlanan modeli oluşturmak üzere olduğunuz sağlayıcılara gösterir. dart:math içe aktarması Random için, flutter/foundation.dart içe aktarması model için debugPrint, model.dart ve BuiltSet uzantısı için utils.dart dahil edilir.

  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(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));

        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, şu anda seçili olan CrosswordSize numaralandırma değerini içeren etkin bir genel değişken olan Size. Bu, kullanıcı arayüzünün yapım aşamasındaki çapraz bulmacanın boyutunu göstermesine ve boyutunu ayarlamasına olanak tanır. İkinci sağlayıcı olan crossword ise daha ilginç bir yapım. Bu, Crossword dizisi döndüren bir işlevdir. Dart'ın jeneratör desteği kullanılarak oluşturulmuştur. Bu destek, işlev üzerinde async* ile işaretlenmiştir. Diğer bir deyişle, sonuç, bir getiriyle sona ermek yerine bir dizi Crossword verir. Bu, ara sonuçlar döndüren bir hesaplama yazmanın çok daha kolay bir yoludur.

crossword sağlayıcı işlevinin başlangıcında bir ref.watch çağrısı çifti bulunduğu için, bulmacanın seçilen boyutu her değiştiğinde ve kelime listesinin yüklenmesi bittiğinde Crosswords akışı Riverpod sistemi tarafından yeniden başlatılır.

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

  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),
        ),
      ),
    );
  }
}

ConsumerWidget olan bu widget, Crossword karakterlerinin gösterileceği ızgaranın boyutunu belirlemek için doğrudan Size sağlayıcısına güvenebilir. Bu ızgara, two_dimensional_scrollables paketindeki TableView widget'ı ile gösterilir.

_buildCell yardımcı işlevi tarafından oluşturulan bağımsız hücrelerin her birinin, döndürülen Widget ağacında bir Consumer widget'ı içerdiğine dikkat edilmesi gerekir. Bu, bir yenileme sınırı görevi görür. ref.watch tarafından döndürülen değer değiştiğinde Consumer widget'ındaki her şey yeniden oluşturulur. Crossword her değiştiğinde tüm ağacı yeniden oluşturmak cazip görünse de birçok hesaplama işlemi yapmak için bu kurulumu kullandığınızda atlanabilir.

ref.watch parametresine bakarsanız crosswordProvider.select sayesinde düzenlerin yeniden hesaplanmasından kaçınıldığınızı görürsünüz. Bu, ref.watch öğesinin yalnızca, hücrenin oluşturulmasından sorumlu olduğu karakter değişiklik olduğunda TableViewCell içeriğinin yeniden oluşturulmasını tetikleyeceği anlamına gelir. Yeniden oluşturmadaki bu azalma, kullanıcı arayüzünün duyarlı kalmasını sağlamanın önemli bir parçasıdır.

CrosswordWidget ve Size sağlayıcısını kullanıcıya göstermek için crossword_generator_app.dart dosyasını şu şekilde 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(),                           // Replaces everything that 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ç değişiklik yapıldı. Öncelikle, wordList öğesini ListView olarak oluşturmaktan sorumlu kod, önceki dosyada tanımlanan CrosswordWidget çağrısıyla değiştirildi. Diğer önemli değişiklik ise kare bulmacanın boyutunu değiştirmekten başlayarak uygulama davranışını değiştirmek için yeni bir menünün sunulması. Sonraki adımlarda daha fazla MenuItemButton eklenecektir. Uygulamanızı çalıştırdığınızda şuna benzer bir şey görürsünüz:

Çapraz kelime oluşturma aracı başlıklı bir uygulama penceresi ve kafiyeli ya da nedensiz, çakışan kelimeler olarak yerleştirilmiş karakterler ızgarası

Izgarada ve menüde, kullanıcının ızgara boyutunu değiştirmesine olanak tanıyan karakterler bulunur. Ancak kelimeler çapraz bulmaca gibi düzenlenmemiş. Bu, kelimelerin çapraz bulmacaya nasıl ekleneceğiyle ilgili herhangi bir kısıtlamanın uygulanmamasının bir sonucudur. Kısacası, ortada bir karmaşa var. Bir sonraki adımda kontrol altına almaya başlayacaksınız.

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

Çapraz kelime kısıtlamalarını uygulamak için modele kod eklemek bu adımın hedefidir. Birçok farklı çapraz bulmaca türü vardır. Bu codelab'in uygulayacağı stil, İngilizce çapraz bulmaca geleneklerinde de geçerlidir. Diğer çapraz bulmaca stillerini oluşturmak için bu kodu değiştirmek, her zamanki gibi okuyucuya bir alıştırma olarak bırakılacaktır.

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

  1. model.dart dosyasını açın ve Crossword modelini şununla 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._();
}

model.dart ve providers.dart dosyalarında yaptığınız değişikliklerin, ilgili model.g.dart ve providers.g.dart dosyalarının güncellenebilmesi için build_runner çalışması gerektiğini hatırlatmak isteriz. Bu dosyalar sihirli bir şekilde kendi kendilerine güncellenmediyse build_runner uygulamasını dart run build_runner watch -d ile 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üklere baktığınızda çok şey oluyor.

Rastgele noktalarda kesişen, aşağı ve yukarı yerleştirilmiş kelimelerin gösterildiği çapraz kelime oluşturma aracı uygulama penceresi

Burada olanları düşünürseniz, şans eseri ortaya çıkan bir çapraz kelime olduğunu görüyoruz. Crossword modelindeki addWord yöntemi, mevcut çapraz kelimeye uymayan herhangi bir önerilen kelimeyi reddediyor. Bu nedenle herhangi bir şeyin yer aldığını görmek şaşırtıcı.

Hangi kelimelerin nerede deneneceğini seçme konusunda daha metodik bir yaklaşım sergilemeye hazırlanırken bu hesaplamayı kullanıcı arayüzü iş parçacığından alıp bir arka plan izolesine taşımak çok yararlı olur. Flutter, büyük miktarda iş alıp bu sarmalayıcıyı arka plan izolesinde (compute işlevi) çalıştırmak için çok kullanışlı bir sarmalayıcı sunar.

  1. providers.dart dosyasında, çapraz kelime sağlayıcıyı aşağıdaki gibi değiştirin:

lib/providers.dart

@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));
        try {
          var candidate = await compute(                   // Edit from here.
              ((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;
    },
  );
}

Bu kod çalışır. Ancak bir tuzak içeriyor. Bu yoldan devam ederseniz sonunda aşağıdaki gibi bir günlüğe kaydedilmiş bir hatayla karşılaşı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 durum, compute hizmetinin bir sağlayıcı üzerinden kapatılamayan arka plan izolesine devredildiği ve SendPort.send() üzerinden gönderilemeyen bir kapatma işleminin sonucudur. Bunun bir düzeltmesi, kapatma işleminin tamamlanması için gönderilemeyecek bir şey olmamasıdır.

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

  1. lib dizininizde bir isolates.dart dosyası oluşturun ve bu dosyaya 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 biraz tanıdık görünecektir. crossword sağlayıcısının temelini oluşturan bu hizmet, artık bağımsız bir oluşturma aracı işlevi görüyor. Arka plan izolasyonunu örneklendirmek için artık 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_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(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();
  }
}
                                                           // Drop the _random instance
@riverpod
Stream<model.Crossword> crossword(CrosswordRef ref) async* {
  final size = ref.watch(sizeProvider);
  final wordListAsync = ref.watch(wordListProvider);

  final emptyCrossword =                                   // Edit from here
      model.Crossword.crossword(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 özellik sayesinde artık farklı boyutlarda çapraz bulmacalar oluşturan bir araç var. compute ile bulmacayı arka plandan izole olarak çözmek için bu araçtan yararlanabilirsiniz. Şimdi, çapraz bulmacaya hangi kelimeleri eklemeye çalışacağınıza karar verirken yalnızca kod daha etkili olabilseydi bunu yapabilirsiniz.

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

Şu anki kodla ilgili sorunun bir kısmı, çözülen sorunun etkili bir şekilde arama problemi olması, mevcut çözümün ise kör arama yapmaktır. Kod, kelimeleri tabloda herhangi bir yere rastgele yerleştirmek yerine mevcut kelimelere eklenecek kelimeleri bulmaya yoğunlaşıyorsa, sistem çözümleri daha hızlı bulur. Bu yaklaşımda, sözcük bulmaya çalışılan konumlardan oluşan bir iş sırası yer alır.

Kod şu anda aday çözümleri oluşturur, aday çözümün geçerli olup olmadığını kontrol eder ve geçerliliğine bağlı olarak adayı dahil eder veya atar. Bu, geri izleme algoritma ailesinden alınan bir örnek uygulamadır. built_value ve built_collection bu uygulama sürecini büyük ölçüde kolaylaştırdı. Böylece türetilen ve dolayısıyla, türetilen sabit değer ile ortak bir durumu paylaşan yeni sabit değerler oluşturulmasını mümkün kılıyor. Bu da derin kopyalama için gereken bellek maliyetleri olmadan potansiyel adayların uygun şekilde kullanılmasını sağlar.

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 birkaç saniyeden uzun süre ekledikten sonra dosyada kırmızı kısa çizgiler kaldıysa build_runner öğenizin hâlâ çalıştığını doğrulayın. Aksi takdirde dart run build_runner watch -d komutunu çalıştırın.

Çeşitli boyutlarda çapraz bulmaca oluşturmanın ne kadar sürdüğünü göstermek için, günlük kaydını tanıtmak üzere olduğunuz kodda. Durations'ın güzel biçimlendirilmiş bir görüntüsü olsaydı güzel olurdu. Neyse ki uzantı yöntemleriyle tam olarak ihtiyacımız olan yöntemi ekleyebiliyoruz.

  1. utils.dart dosyasını şu şekilde 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 uzantı yöntemi, saniyelerden günlere kadar değişen farklı süreleri görüntülemek için uygun yöntemi seçmek amacıyla kayıtlar yerine geçiş ifadeleri ve kalıp eşleştirmeden yararlanır. Bu kod stili hakkında daha fazla bilgi için Darat'ın kalıplarına ve kayıtlarına dair ayrıntılı bilgi codelab'ine bakın.

  1. Bu yeni işlevi entegre etmek için, isolates.dart dosyasını exploreCrosswordSolutions işlevinin tanımını aşağıdaki gibi yeniden tanımlayacak ş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<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 yüzeyde aynı görünen bir uygulama oluşur, ancak aradaki fark, tamamlanmış bir çapraz bulmacanın ne kadar sürede bulunacağıdır. İşte 1 dakika 29 saniyede oluşturulmuş 80 x 44 boyutunda bir çapraz bulmaca.

Kesişen birçok kelimenin bulunduğu Bulmaca Oluşturucu. Uzaklaştırıldığında kelimeler okunamayacak kadar küçük.

Elbette, daha hızlı ilerleyebilir miyiz? Evet, yapabiliriz.

7. Yüzey istatistikleri

Bir şeyi hızlandırmada neler olup bittiğini görmek faydalı olur. Süreç devam ederken ona yardımcı olan şeylerden biri de süreçle ilgili bilgileri ortaya çıkarmaktır. Dolayısıyla, şimdi enstrümanlar ekleyip bu bilgileri fareyle üzerine gelinen bilgi paneli olarak görüntülemenin zamanı geldi.

Görüntüleyeceğiniz bilgilerin WorkQueue'den çıkarılması ve kullanıcı arayüzünde görüntülenmesi gerekir.

Görüntülemek istediğiniz bilgileri içeren yeni bir model sınıfı tanımlamak, yararlı bir ilk adımdı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. DisplayInfo sınıfını eklemek için dosyanın sonunda 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ı değiştirerek WorkQueue modelini aşağıdaki gibi gösterin:

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ş sırasını açığa çıkardığı için şimdi bu veri kaynağından istatistiklerin nasıl ve nereden çıkarılacağı sorusu geliyor.

  1. Eski çapraz kelime sağlayıcısını bir iş sırası sağlayıcısıyla değiştirin ve ardından iş sırası sağlayıcısının akışından bilgi alan daha fazla sağlayıcı 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_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(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();
  }
}

@riverpod
Stream<model.WorkQueue> workQueue(WorkQueueRef 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(ExpectedRemainingTimeRef 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, bilgi görüntüsünün çapraz bulmacanın üzerine yerleştirilip yerleştirilmeyeceği biçiminde global durumun ve bulmaca oluşturma sürecinin çalışma süresi gibi türetilmiş verilerin bir karışımıdır. Tüm bunlar, bu durumun bir kısmını dinleyenlerin geçici olması nedeniyle karmaşıktır. Bilgi ekranı gizlendiğinde hiçbir şey çapraz kelime hesaplamasının başlangıç ve bitiş zamanlarını dinleyemez, ancak bilgi ekranı gösterildiğinde hesaplamanın doğru olması durumunda bu işlemlerin bellekte kalması gerekir. Bu durumda Riverpod özelliğinin keepAlive parametresi çok yararlıdır.

Bilgi ekranını gösterirken küçük bir kırışıklık var. O anda geçen çalışma süresini gösterebilmeyi istiyoruz ancak burada, geçen süreyi sabit bir şekilde güncellemeyi kolayca zorlayacak bir şey yok. Flutter'da yeni nesil kullanıcı arayüzleri oluşturma codelab'ine geri döndüğünüzde, bu gereksinim için faydalı bir widget aşağıda verilmiştir.

  1. lib/widgets dizininde bir ticker_builder.dart dosyası oluşturun ve aşağıdaki içeriği bu dosyaya 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 balyoz. Her karede içeriğini yeniden oluşturur. Bu durum genellikle hoş karşılanmaz, ancak çapraz bulmaca için yapılan aramaların işlem yüküyle karşılaştırıldığında, her karenin geçen süreyi yeniden boyaması işlem yükü muhtemelen gürültüde kaybolacaktır. Yeni elde edilen bu 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 bu dosyaya aşağıdaki içeriği ekleyin:

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 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 gücünün güzel bir örneği. 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'ın kullanıcı arayüzüne entegre edilmesidir.

  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(
        menu Children: [
          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 etmeyle ilgili farklı yaklaşımları göstermektedir. CrosswordGeneratorApp ürününün build yönteminde, bilgi ekranı gösterildiğinde veya gizlendiğinde yeniden oluşturulmaya zorlanan alanı kapsayacak yeni bir Consumer oluşturucuyu kullanıma sundunuz. Diğer yandan, tek bir açılır menü var ConsumerWidget, bulmacanın yeniden boyutlandırılması veya bilgi ekranının gösterilmesi ya da gizlenmesi gibi durumlarda yeniden oluşturulur. Tercih edilen yaklaşım, yeniden oluşturulan widget ağaçlarının yerleşimlerini yeniden hesaplama maliyetine karşı basitlikten ödün vermemektir.

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 çapraz bulmaca üretiminin sonuna doğru sayıların değiştiği bir dönem görüyoruz, ancak karakter tablosunda çok az değişiklik oluyor.

Çapraz Kelime Oluşturucu uygulama penceresi, bu kez daha küçük, algılanabilir kelimeler ve sağ alt köşede mevcut nesil çalıştırma istatistiklerinin yer aldığı kayan bir yer paylaşımı

Neler olduğu konusunda daha fazla bilgi edinmek faydalı olacaktır.

8. İleti dizileriyle paralel yapma

Sürecin sonunda işlerin neden yavaşladığını anlamak için algoritmanın yaptığı işlemleri görselleştirmek faydalı olacaktır. WorkQueue bölgesinde göze çarpan locationsToTry var. TableView bunu araştırmamız için bize kullanışlı bir yol sunar. Hücre rengini, locationsToTry içinde olup olmadığına göre 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 bekleyen konumların bir görselleştirmesini görürsünüz.

Kuşağın yarısını gösteren çapraz kelime oluşturma aracı. Bazı harflerin koyu mavi arka plan üzerinde beyaz metin, bazılarında ise beyaz arka plan üzerinde mavi metin bulunur.

Buradaki ilginç şey, kare bulmacayı sonuna doğru ilerledikçe izlemenin ilginç bir yanı da, incelenmesi gereken bir dizi nokta bulunması ve bu yüzden hiçbir yararlı sonuçla karşılaşmamaktır. Birkaç seçeneğiniz var: Birincisi, bulmacanın belirli bir yüzdesi doldurulduktan sonra araştırmayı sınırlamak, ikincisi ise aynı anda birden çok önemli yeri araştırmaktır. İkinci yol kulağa daha eğlenceli geliyor, öyleyse yapalım.

  1. isolates.dart dosyasını düzenleyin. Bu, bir arka plan izolesinde hesaplanan kodu N arka plan izolasyonu havuzuna bölmek amacıyla yeniden yazılan bir koddur.

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);
}

Temel iş mantığı değişmediğinden bu kodun büyük bir kısmına aşina olmanız gerekir. Değişen şey, artık iki katmanlı compute çağrıları olmasıdır. İlk katman, N çalışan izolasyonunda arama yapmak üzere ayrı ayrı pozisyonlar oluşturmaktan ve tüm N çalışan izolasyonu tamamlandığında sonuçları yeniden birleştirmekten sorumludur. İkinci katman, N çalışan izolesinden oluşur. En iyi performansı elde etmek için N ayarını yapmak hem bilgisayarınıza hem de söz konusu verilere bağlıdır. Çizelge ne kadar büyükse o kadar çok çalışan birbirlerine engel olmadan birlikte çalışabilir.

İlginç bir nokta da, yakalamaması gereken şeylerin yakalanmasıyla ilgili olarak bu kodun şu anda nasıl işlediğine dikkat etmek. Şu anda kapalı bölüm yok. _generate ve _generateWorker işlevleri, yakalama için çevresi olmayan üst düzey işlevler olarak tanımlanır. İki işlevin de argümanları ve sonuçları Dart kayıtları biçimindedir. Bu, compute çağrısının anlamında bir değer, bir değer anlamına gelir.

Artık çapraz bulmaca oluşturmak üzere bir ızgarada birbirine kenetlenen kelimeleri aramak için arka plan çalışanlarından oluşan bir havuz oluşturabildiğinize göre, şimdi bu yeteneği bulmacanın diğer kısımlarında da kullanabilirsiniz.

  1. WorkQueue sağlayıcısını aşağıdaki gibi düzenleyerek providers.dart dosyasını düzenleyin:

lib/providers.dart

@riverpod
Stream<model.WorkQueue> workQueue(WorkQueueRef 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. WorkerCount sağlayıcısını aşağıdaki gibi dosyanın sonuna 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ı, arka plan izole havuzu için maksimum çalışan sayısını, yalıtım işlevlerinin doğru şekilde yapılandırılacağı bir yöntemle artık kullanıma sunuyor.

  1. CrosswordInfoWidget öğesini aşağıdaki gibi değiştirerek crossword_info_widget.dart dosyasını 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 these two lines
                        label: 'Max worker count', value: workerCount),
                    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. _CrosswordGeneratorMenu widget'ına aşağıdaki bölümü ekleyerek crossword_generator_app.dart dosyasını 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 bulmacada kullanılacak kelimeleri aramak için örneklenen arka plan izolelerinin sayısını değiştirebilirsiniz.

  1. Çapraz bulmacanın boyutlarını içeren içerik menüsünü açmak için dişli simgesini tıklayarak o anda oluşturulmuş çapraz bulmacayla ilgili istatistiklerin ve şimdi kullanılacak izole sayısının görüntülenmesini isteyip istemediğinizi belirtin.

Kelimeler ve istatistiklerin yer aldığı çapraz kelime oluşturma aracı penceresi

Çapraz bulmacanın çalıştırılması, aynı anda birden fazla çekirdeği kullanarak 80x44 çapraz bulmacaların işlem süresini önemli ölçüde azalttı.

9. Oyuna dönüştürün

Bu son bölüm gerçekten bir bonus tur. Bulmaca oluşturucuyu geliştirirken öğrendiğiniz tüm teknikleri kullanacak ve bu teknikleri kullanarak bir oyun geliştireceksiniz. Bir çapraz bulmaca oluşturmak için çapraz kelime oluşturma aracını kullanacaksınız. Kullanıcının ızgaradaki kelime şeklindeki çeşitli deliklere koyacak kelimeleri seçmesini ve seçimlerini kaldırmasını sağlamak için bağlamsal menü deyimlerini yeniden kullanırsınız. Bunların amacı bulmacayı tamamlamaktır.

Bu oyunun iyi olduğunu veya bittiğini söylemeyeceğim, aslında pek de kolay olmadığını söyleyeyim. Alternatif kelime seçiminin iyileştirilmesiyle çözülebilecek denge ve zorluk sorunları vardır. Kullanıcıları içeriklerinize yönlendirecek bir eğitim yok ve düşünme animasyonu anlaşılacak pek çok şey bırakıyor. Kimi zaman "Kazandınız!" diye sormayacağım bile. tıklayın.

Bunun karşılığında, bu proto oyunu düzgün bir şekilde tam oyuna dönüştürmek çok daha fazla kod gerektiriyor. Tek bir codelab'de olması gerekenden daha fazla kod. Bu, bu codelab'de şimdiye kadar öğrenilen teknikleri, bunların nerede ve nasıl kullanılacağını değiştirerek güçlendirmek için tasarlanmış bir hızlı koşu adımıdır. Bu açıklamanın, bu codelab'de daha önce öğrenilenleri pekiştirdiğini umuyoruz. Alternatif olarak, devam edip bu koda dayalı kendi deneyimlerinizi oluşturabilirsiniz. Neler geliştirdiğinizi görmekten memnuniyet duyarız.

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

  1. lib/widgets dizinindeki her şeyi silin. Oyununuz için göz alıcı yeni widget'lar oluşturacaksınız. Bu durum, eski widget'lardan epey ödünç almak anlamına geliyor.
  2. Crossword ürününün addWord yöntemini aşağıdaki şekilde güncellemek için model.dart dosyanızı 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;
    }
  }

Çapraz kelime modelinizde yapılan bu küçük değişiklik, çakışmayan kelimelerin eklenmesine olanak tanır. Oyuncuların masa üzerinde herhangi bir yerde oynamalarına izin vermek ve Crossword oyuncunun hamlelerini depolamak için temel model olarak kullanmaya devam edebilmesini sağlar. Bu yalnızca, belirli bir yöne yerleştirilmiş, belirli konumlarda bulunan kelimelerin bir listesidir.

  1. model.dart dosyanızın sonuna CrosswordPuzzleGame model sınıfını 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, değişikliklere neden olan ilgi çekici bilgilerdir. İstatistik toplama işlemine destek veren sağlayıcıların çoğu kaldırıldı. Arka plan izolelerinin sayısını değiştirme özelliği kaldırıldı ve sabit değer ile değiştirildi. Ayrıca, yukarıda eklediğiniz yeni CrosswordPuzzleGame modeline erişim sağlayan yeni bir sağlayıcı da var.

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_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(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();
  }
}

@riverpod
Stream<model.WorkQueue> workQueue(WorkQueueRef 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ısının en ilginç yönleri, CrosswordPuzzleGame öğelerini bir Crossword ve bir wordList üzerinden oluşturma ve kelime seçme masrafını telafi etmek için uygulanan stratejilerdir. Bu işlemlerin her ikisi de arka plan Yalıtımı yardımı olmadan gerçekleştirildiğinde kullanıcı arayüzü etkileşiminde yavaşlığa neden olur. Arka planda nihai sonucu hesaplarken ara sonucu ilerletmek için elinizi çabuk tutarak gerekli hesaplamalar arka planda devam ederken duyarlı bir kullanıcı arayüzü oluşturursunuz.

  1. Şu anda boş olan lib/widgets dizininde aşağıdaki içeriklerle 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 büyük bir kısmına artık aşina olmanız gerekir. Evet, tanımlanmamış widget'lar olacak ve bunları düzeltmeye başlayacaksınız.

  1. Bir crossword_generator_widget.dart dosyası oluşturun ve dosyaya şu 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 yeterince aşina olmanız gerekir. Aradaki temel fark, oluşturulan kelimelerin karakterlerini görüntülemek yerine, bilinmeyen bir karakterin varlığını belirtmek için artık bir unicode karakteri görüntülemenizdir. Bu, estetiğin iyileştirilebilmesi için birkaç çalışma gerektirebilir.

  1. crossword_puzzle_widget.dart dosyası oluşturun ve dosyaya şu 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ılan parçalardan yapılmış olmasına rağmen bir öncekinden biraz daha yoğun. Artık, doldurulan her hücre tıklandığında, kullanıcının seçebileceği kelimelerin listelendiği bir içerik menüsü oluşturulur. Kelimeler seçildiyse, çelişen kelimeler seçilemez. Kullanıcı, bir kelimenin seçimini kaldırmak için o kelimenin menü öğesine dokunur.

Oyuncunun bulmacanın tamamını dolduracak kelimeleri seçebildiğini varsayarsak, size bir "Kazandınız!" tıklayın.

  1. Bir puzzle_completed_widget.dart dosyası oluşturun ve ardından aşağıdaki içeriği bu dosyaya 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,
        ),
      ),
    );
  }
}

Eminim bunu alıp daha ilgi çekici hale getirebilirsin. Animasyon araçları hakkında daha fazla bilgi edinmek için Flutter'da yeni nesil kullanıcı arayüzleri oluşturma codelab'ine bakın.

  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(
          useMaterial3: true,
          colorSchemeSeed: Colors.blueGrey,
          brightness: Brightness.light,
        ),
        home: CrosswordPuzzleApp(),                         // Update this line
      ),
    ),
  );
}

Bu uygulamayı çalıştırdığınızda, çapraz kelime oluşturucu bulmacanızı oluştururken animasyonu görürsünüz. Ardından, çözmeniz gereken boş bir bulmacayla karşılaşacaksınız. Sorunu çözdüğünüzü varsayarsak, şuna benzer bir ekranla karşılaşacaksınız:

&#39;Bulmaca tamamlandı!&#39; metnini gösteren çapraz bulmaca uygulama penceresi

10. Tebrikler

Tebrikler! Flutter ile başarılı bir bulmaca oyunu geliştirdiniz.

Bulmaca oyununa dönüşen bir çapraz kelime oluşturma aracı geliştirdiniz. İzole havuzda arka plan hesaplamaları yapma konusunda uzmanlaştınız. Geri izleme algoritmasının uygulanmasını kolaylaştırmak için sabit veri yapıları kullandınız. Ayrıca, TableView ile kaliteli zaman harcadınız. Bu özellik, bir dahaki sefere tablo halindeki verileri görüntülemek istediğinizde kullanışlı olacaktır.

Daha fazla bilgi