Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Watch doesn't trigger rebuild on signal if used in a nested Builder #363

Open
SPiercer opened this issue Dec 14, 2024 · 4 comments
Open

Comments

@SPiercer
Copy link

there's another weird behaviour i have found when using signals in such way

so i have declared my signals as usual

final selectedPostSignal = signal<SelectedPost?>(null);

and in my widget down below i'm using

Watch.builder(
  builder: (_){
    return Column(
      children: [
        SomeWidget(),
        SomeOtherWidget(),
        if (selectedPostSignal.value?.id == post.id)
          PostDetails(post: post),
      ],
    )
  }
)

if i did this it would never work and never rebuild if somewhere else i changed selectedPostSignal

i have to assign it to a variable to work

Watch.builder(
  builder: (_){
    final selectedPost = selectedPostSignal.value;
    return Column(
      children: [
        SomeWidget(),
        SomeOtherWidget(),
        if (selectedPost?.id == post.id)
          PostDetails(post: post),
      ],
    )
  }
)

and volla!, magically it works.

please validate this with me rody, if that's a malpractice of me.

@rodydavis
Copy link
Owner

This should be equivalent! Not sure why one is working and the other is not.

Do you have a complete example to test with?

@SPiercer
Copy link
Author

It's quite strange that when i made a simple counter app example, it worked flawlessly.


but for if you can read it out idk if i'm doing anything wrong in this app here but

that's the page i'm working on atm
it's a legacy code we're running on 5.5.0 and the code is bad but i'm refactoring atm

i redacted many irrelevant widgets for you to read easier

final selectedPostSignal = Ref.scoped(
  (_) => signal<SocialPost?>(null, debugLabel: 'selectedPostSignal'),
  autoDispose: false,
);

final selectedProfileSignal = Ref.scopedFamily(
  (_, int customerId) => futureSignal<SocialProfile>(
    () async {
      return Supabase.instance.client
          .from('social_profiles')
          .select('*, posts:social_posts(*)')
          .eq('customer_id', customerId)
          .single()
          );
    },
    debugLabel: 'selectedProfileSignal',
  ),
  autoDispose: false,
);

class SocialProfileScreen extends StatefulWidget {
  final int? customerId;
  const SocialProfileScreen({super.key, this.customerId});

  @override
  State<SocialProfileScreen> createState() => _SocialProfileScreenState();
}

class _SocialProfileScreenState extends State<SocialProfileScreen>
    with SignalsMixin {

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Text(
          'Social management',
          style: context.textTheme.displaySmall
              ?.copyWith(fontWeight: FontWeight.w600),
        ),
        const Gap(16),
        Expanded(
          child: Row(
            children: [
              Watch.builder(
                builder: (context) {
                  final profileSignal =
                      selectedProfileSignal.of(context, widget.customerId);

                  final selectedPost = selectedPostSignal.of(context).value;

                  return profileSignal.value.map(
                    loading: () => CenterLoading(
                      indicatorColor: context.colorScheme.primary,
                    ),
                    error: (error) => Center(child: Text('Error: $error')),
                    data: (profile) => Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        const SocialTabs(),
                        ReorderableGridView.builder(
                          itemBuilder: (context, index) {
                            final post = posts.value[index];

                            return Stack(
                              key: ValueKey(index),
                              children: [
                                PostCard(
                                  post: post,
                                  isPinned: post.isPinned,
                                  onTap: (post) => selectedPostSignal
                                      .of(context)
                                      .value = post,
                                ),
                                if (selectedPost?.id == post.id)
                                  Text(post.postDateFormatted),
                              ],
                            );
                          },
                        ),
                      ],
                    ),
                  );
                },
              ),
              const Gap(16),
              Watch.builder(
                builder: (context) {
                  final selectedPost = selectedPostSignal.of(context);
                  if (selectedPost.value == null) return const SizedBox();
                  return const Expanded(child: CommentsView());
                },
              ),
            ],
          ),
        ),
      ],
    );
  }
}

so if we zoom in on the ReordableGridView

that's the working example where i have selectedPost assigned to a variable

ReorderableGridView.builder(
  itemBuilder: (context, index) {
    final post = posts.value[index];

    return Stack(
      key: ValueKey(index),
      children: [
        PostCard(
          post: post,
          isPinned: post.isPinned,
          onTap: (post) => selectedPostSignal
              .of(context)
              .value = post,
        ),
        if (selectedPost?.id == post.id)
          Text(post.postDateFormatted),
      ],
    );
  },
),

but if I changed it to be if (selectedPostSignal .of(context) .value?.id == post.id)

it never get updated from the onTap thus the if condition is never met EXCEPT FOR THE FIRST TIME ONLY when it was null and then got assigned to a value, then it never gets reassigned/updated

@rodydavis
Copy link
Owner

That is because .of(context) is inside a builder context for the list view and not a Watch.builder.

The signal would not be updated because it is only reading the value and not bound to that context.

You could try .watch(context)

@SPiercer
Copy link
Author

You are right!

it's because of the listview context not the Watch.builder, however it's not about the .of(context) i removed it in the example is got the weird behaviour

i tried using .watch but still same issue, things inside builders don't count. i was testing the counter app on latest version though.

here's the counter app example

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

final selectedCounter = signal<int?>(null, debugLabel: 'selectedCounter');
void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    _counter++;
    if (_counter == 3) {
      selectedCounter.value = 3;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Watch.builder(
          debugLabel: 'counter widget',
          builder: (context) {
            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
                // Comment these two lines to watch the strange behavior
                if (selectedCounter.watch(context) == _counter)
                  const Text('Without Builder: EQUAL'),
                Builder(builder: (_) {
                  if (selectedCounter.watch(context) == _counter) {
                    return const Text('With Builder: EQUAL');
                  } else {
                    return const Text('With Builder: NOT EQUAL');
                  }
                })
              ],
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

@SPiercer SPiercer changed the title Watch doesn't trigger rebuild on signal if not used _explictly_ Watch doesn't trigger rebuild on signal if used in a nest Builder Dec 14, 2024
@SPiercer SPiercer changed the title Watch doesn't trigger rebuild on signal if used in a nest Builder Watch doesn't trigger rebuild on signal if used in a nested Builder Dec 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants