状態管理
Zylix は集中型のバージョン追跡付き状態管理を採用しています。すべてのアプリケーション状態は Zig で管理され、プラットフォームシェルには読み取り専用で公開されます。
設計原則
- 単一の信頼源: グローバルストアがすべてのアプリケーションデータを所有
- 不変更新: 状態遷移は新しいバージョンを作成
- バージョン追跡: すべての変更にバージョン番号を割り当て
- 差分検出: 変更を追跡して効率的なレンダリングを実現
状態構造
アプリケーション状態
pub const AppState = struct {
/// カウンター値
counter: i64 = 0,
/// フォーム入力
input_text: [256]u8 = [_]u8{0} ** 256,
input_len: usize = 0,
/// Todo アイテム
todos: [MAX_TODOS]Todo = undefined,
todo_count: usize = 0,
/// 現在のフィルター
filter: Filter = .all,
/// ビューデータポインタを取得(ABI 用)
pub fn getViewData(self: *const AppState) ?*const anyopaque {
return @ptrCast(self);
}
/// ビューデータサイズを取得(ABI 用)
pub fn getViewDataSize(_: *const AppState) usize {
return @sizeOf(AppState);
}
};
pub const Todo = struct {
id: u32,
text: [128]u8,
text_len: usize,
completed: bool,
};
pub const Filter = enum(u8) {
all = 0,
active = 1,
completed = 2,
};UI 状態
pub const UIState = struct {
/// 現在の画面
screen: Screen = .home,
/// ローディング状態
loading: bool = false,
pub const Screen = enum(u32) {
home = 0,
detail = 1,
settings = 2,
};
};統合状態
pub const State = struct {
/// 状態バージョン(単調増加)
version: u64 = 0,
/// アプリケーション固有の状態
app: AppState = .{},
/// UI 状態ヒント
ui: UIState = .{},
/// 最後のエラーメッセージ
last_error: ?[]const u8 = null,
/// 状態変更後にバージョンを増加
pub fn bumpVersion(self: *State) void {
self.version +%= 1;
}
};ジェネリックストア
Store は型安全な状態管理を提供します。
pub fn Store(comptime T: type) type {
return struct {
const Self = @This();
current: T,
previous: T,
version: u64 = 0,
dirty: bool = false,
pub fn init(initial: T) Self {
return .{
.current = initial,
.previous = initial,
};
}
/// 現在の状態を取得(読み取り専用)
pub fn getState(self: *const Self) *const T {
return &self.current;
}
/// 変更可能な状態を取得(内部使用)
pub fn getStateMut(self: *Self) *T {
return &self.current;
}
/// 前の状態を取得(差分検出用)
pub fn getPrevState(self: *const Self) *const T {
return &self.previous;
}
/// 変更をコミット
pub fn commit(self: *Self) void {
if (self.dirty) {
self.previous = self.current;
self.version += 1;
self.dirty = false;
}
}
/// 関数で状態を更新してコミット
pub fn updateAndCommit(self: *Self, update_fn: *const fn (*T) void) void {
update_fn(&self.current);
self.dirty = true;
self.commit();
}
};
}状態アクセス
状態の読み取り
const state = @import("state.zig");
// 現在の状態を取得(読み取り専用)
const current = state.getState();
std.debug.print("カウンター: {d}\n", .{current.app.counter});
// バージョンを取得
const version = state.getVersion();
std.debug.print("バージョン: {d}\n", .{version});
// 初期化状態を確認
if (state.isInitialized()) {
// 状態は準備完了
}リデューサー
状態変更はリデューサー関数を通じて処理されます。
/// インクリメントイベントを処理
pub fn handleIncrement() void {
const increment = struct {
fn f(app: *AppState) void {
app.counter += 1;
}
}.f;
global_store.updateAndCommit(&increment);
_ = calculateDiff();
}
/// デクリメントイベントを処理
pub fn handleDecrement() void {
const decrement = struct {
fn f(app: *AppState) void {
app.counter -= 1;
}
}.f;
global_store.updateAndCommit(&decrement);
_ = calculateDiff();
}
/// リセットイベントを処理
pub fn handleReset() void {
const reset_counter = struct {
fn f(app: *AppState) void {
app.counter = 0;
}
}.f;
global_store.updateAndCommit(&reset_counter);
_ = calculateDiff();
}テキスト入力の処理
複雑な状態更新の例:
/// テキスト入力イベントを処理
pub fn handleTextInput(text: []const u8) void {
const app = global_store.getStateMut();
// テキストを状態バッファにコピー
const copy_len = @min(text.len, app.input_text.len - 1);
@memcpy(app.input_text[0..copy_len], text[0..copy_len]);
app.input_text[copy_len] = 0; // null 終端
app.input_len = copy_len;
// dirty フラグを設定してコミット
global_store.dirty = true;
global_store.commit();
_ = calculateDiff();
}差分追跡
Zylix は状態バージョン間の変更を追跡します。
pub fn Diff(comptime T: type) type {
return struct {
const Self = @This();
changed: bool = false,
version: u64 = 0,
fields_changed: u32 = 0,
pub fn init() Self {
return .{};
}
pub fn calculate(old: *const T, new: *const T, version: u64) Self {
var result = Self{
.version = version,
};
// フィールドを比較して変更を検出
if (!std.mem.eql(u8, std.mem.asBytes(old), std.mem.asBytes(new))) {
result.changed = true;
result.fields_changed = countChangedFields(old, new);
}
return result;
}
};
}差分の使用
// 状態変更後に差分を計算
const diff = state.calculateDiff();
if (diff.changed) {
std.debug.print("状態が変更されました!フィールド数: {d}\n", .{diff.fields_changed});
// 再レンダリングをトリガー
reconciler.scheduleRender();
}ABI 互換状態
クロス言語相互運用のために、状態は C ABI を通じて公開されます。
/// C ABI 互換の状態構造体
pub const ABIState = extern struct {
version: u64,
screen: u32,
loading: bool,
error_message: ?[*:0]const u8,
view_data: ?*const anyopaque,
view_data_size: usize,
};
// ABI 形式に変換
pub fn toABI(self: *const State) ABIState {
return .{
.version = self.version,
.screen = @intFromEnum(self.ui.screen),
.loading = self.ui.loading,
.error_message = if (self.last_error) |err|
@ptrCast(err.ptr)
else
null,
.view_data = self.app.getViewData(),
.view_data_size = self.app.getViewDataSize(),
};
}プラットフォームからのアクセス
// Swift
let state = zylix_get_state()
print("カウンター: \(state.pointee.counter)")// Kotlin
val state = ZylixBridge.getState()
println("カウンター: ${state.counter}")// JavaScript (WASM)
const state = zylix.getState();
console.log(`カウンター: ${state.counter}`);// C#
var statePtr = ZylixInterop.GetState();
var state = Marshal.PtrToStructure<ZylixState>(statePtr);
Console.WriteLine($"カウンター: {state.counter}");メモリアリーナ
Zylix は一時的な状態操作にアリーナアロケーションを使用します。
/// 一時アロケーション用のスクラッチアリーナ
var scratch_arena: Arena(4096) = Arena(4096).init();
/// スクラッチアリーナを取得
pub fn getScratchArena() *Arena(4096) {
return &scratch_arena;
}
/// スクラッチアリーナをリセット(各イベントディスパッチサイクル後に呼び出し)
pub fn resetScratchArena() void {
scratch_arena.reset();
}スクラッチアリーナの使用
// 一時作業用のアリーナを取得
const arena = state.getScratchArena();
// 一時バッファを割り当て
const buf = arena.alloc(u8, 256) orelse return;
// バッファを使用
formatMessage(buf, "Hello");
// 処理後にリセット(ディスパッチサイクル内)
state.resetScratchArena();ライフサイクル
初期化
pub fn init() void {
global_store = Store(AppState).init(.{});
global_state = .{};
last_diff = Diff(AppState).init();
scratch_arena.reset();
initialized = true;
}終了処理
pub fn deinit() void {
global_store = Store(AppState).init(.{});
global_state = .{};
last_diff = Diff(AppState).init();
scratch_arena.reset();
initialized = false;
}ベストプラクティス
1. 状態をフラットに保つ
// 良い例: フラットな状態
pub const AppState = struct {
todos: [MAX_TODOS]Todo = undefined,
todo_count: usize = 0,
selected_id: ?u32 = null,
filter: Filter = .all,
};
// 悪い例: 深くネストした状態
pub const AppState = struct {
ui: struct {
list: struct {
items: struct {
todos: [MAX_TODOS]Todo,
},
},
},
};2. 有限状態に列挙型を使用
// 良い例: 明示的な状態
pub const LoadingState = enum {
idle,
loading,
success,
error,
};
// 悪い例: ブールフラグ
pub const State = struct {
is_loading: bool,
has_error: bool,
is_success: bool, // 不整合な状態が可能
};3. レンダリング前にバージョンチェック
var last_rendered_version: u64 = 0;
fn shouldRender() bool {
const current_version = state.getVersion();
if (current_version > last_rendered_version) {
last_rendered_version = current_version;
return true;
}
return false;
}4. 関連する更新をバッチ処理
// 良い例: 関連する変更を単一コミット
pub fn handleTodoComplete(id: u32) void {
const app = global_store.getStateMut();
// 複数の関連更新
if (findTodo(app, id)) |todo| {
todo.completed = true;
app.completed_count += 1;
app.active_count -= 1;
}
// 単一コミット
global_store.dirty = true;
global_store.commit();
}テスト
test "状態の初期化" {
init();
try std.testing.expect(isInitialized());
try std.testing.expectEqual(@as(u64, 0), getVersion());
deinit();
try std.testing.expect(!isInitialized());
}
test "カウンターのインクリメント" {
init();
defer deinit();
try std.testing.expectEqual(@as(i64, 0), getState().app.counter);
handleIncrement();
try std.testing.expectEqual(@as(i64, 1), getState().app.counter);
try std.testing.expectEqual(@as(u64, 1), getVersion());
handleIncrement();
try std.testing.expectEqual(@as(i64, 2), getState().app.counter);
try std.testing.expectEqual(@as(u64, 2), getVersion());
}