Friday, 22 November 2024

A Commodore 64 Emulator in Flutter: Part 2

Foreword

In the previous post I gave introduction to my idea of creating a C64 Emulator in Flutter.

I explained briefly how to install Flutter and configuring an IDE for using it and looked at a Hello World project created by IntelliJ.

In this post we will continue our journey on creating a C64 emulator in Flutter.

Introduction to using BloCs

Let us look a bit more into using BloCs. From the previous post we know that BloC stands for Business Logic component.

The term describes it all, which is splitting your business logic into a component. This imply having a stateless widget. The state will be kept in the BloC component, and when state changes, it will send the changed state to the widget, so that the widget will be re-rendered with the new state.

When using BloCs you basically need to remember two classes: BlocProvider and BlocBuilder. With BlocProvider, you create an instance of your BloC. BlocProvider then allows you to inject this instance further down in widget your tree by means of a BlocBuilder.

To use these classes in your widget tree, you should first create a proper BloC class, which I will discuss in the next section

BloC, Event and State

Let us create BloC class. Two things a Bloc class needs is an Event class and a State class.

You might remember from the previous post, I was illustrating by means of a diagram explaining what the purpose of an Event and a State were.

An Event you would use, for instance, when you click a button in your flutter app and you want your BloC to react in a certain way. You would then trigger an Event when you click a button, and your BloC will listen for that event.

In response to the event, your BloC will do some processing and afterwards it might have some data you want to display in your widget. In this case, your Widget will emit State, which your BlocBuilder will use to construct a widget for display.

Let us start by creating an Event class. Our Event class will be extending Equatable, for which we need to add the following dependency in pubspec.yaml:

equatable: ^2.0.5
pubspec.yaml is like a project file in Flutter hosting different settings and dependencies. I will not be going into details of the pubspec.yaml here. There is quite a few websites going into depth about this file. However, I will mention if there is things that needs to be added to this file for our project.

With this dependency added, and Pub get being run, we can now create our State class:

import 'package:equatable/equatable.dart';

class C64State extends Equatable {
  @override
  // TODO: implement props
  List<Object?> get props => throw UnimplementedError();
}
Similarly we create an event class:

import 'package:equatable/equatable.dart';

class C64Event extends Equatable {
  @override
  // TODO: implement props
  List<Object?> get props => throw UnimplementedError();
  
}
We are now ready to create our BloC, but first we need add the following dependency:

flutter_bloc: ^8.1.3
Now we can create our BloC class:

import 'package:flutter_bloc/flutter_bloc.dart';

import 'c64_event.dart';
import 'c64_state.dart';

class C64Bloc extends Bloc<C64Event, C64State> {
  C64Bloc(): super(C64State());

}
Our created classes looks fairly empty, but it will get more body as go along. One thing you will notice with our C64Bloc class is that we pass an instance of C64State() to our super. This is our initial state and will force a render of our Stateless widget. There will be subsequent state change for which we will use emit() to update our UI. More on this later.

Let us now move onto using the BlocProvider class. Let us refresh ourselves on how our app class looks again:

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo 2',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

We will add the BlocProvider instance in the home: selector:

      home: BlocProvider(
        create: (context) => C64Bloc(),
        child: const MyHomePage(title: 'Flutter Demo Home Page'),
      )
So, in the create selector we create an instance of our Bloc which will be passed down our widget tree.

Th original instance of MyHomePage now moved down to the home selector.

Now somewhere within our MyHomePage class, we need to inject the created BloC via BlocBuilder.

However, let do first things first. IntelliJ previously created our MyHomePage as a stateful widget, but we want a stateless widget. This is easy enough and we change our class to extend from StatelessWidget instead of StatefulWidget.

Having made the change, The IDE complains that MyHomeClass now needs a build method. So, we let the IDE implement this method. With all this our MyHomePage class implifies to the following:

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

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    throw UnimplementedError();
  }
}
The IDE has created a TODO for us.  Let us implement it straight by making use of a BlockBuilder:

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

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<C64Bloc, C64State>(
      builder: (BuildContext context, state) {
        return Text('Hi There!!');
      },
    );
  }
}
And everything renders in the browser:
Our text is weirdly formatted in red and underlined because we did not surround it with a proper widget for applying styling. This will be addressed at a later stage.

As mentioned earlier when we create an instance of C64Bloc, an initial state is passed to its Super method. It is this initial state that triggers the drawing of the text when the app starts up. 

It would actually be nice to show something of our initial state in our widget, just to get a feel that everything is working end to end.

For this purpose, let us add an epoch to our C64State class:

class C64State extends Equatable {
  final time = DateTime.now().millisecondsSinceEpoch;
...
}
We can now adjust our MyHomePage class as follows:

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

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<C64Bloc, C64State>(
      builder: (BuildContext context, state) {
        return Text(state.time.toString());
      },
    );
  }
}

And the rendered page look like this:

Our Epoch gets displayed!!

Using Events

So, in the previous section we basically just shown a timestamp once off, when the app started up. But what if we wanted to update the timestamp on the screen when you press a button?

This is where events come in. When you click a button, an event will be fired which our BloC class will listen for and update the state.

Before we can do this, we need to wrap our BlocBuilder into a more meaningful container, which will allow us to easily add a button. For this we wrap our BlocBuilder into a Scaffold, which will move our BlocBuilder to the body attribute:

  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('BLoC Example')),
      body: BlocBuilder<C64Bloc, C64State>(
        builder: (BuildContext context, state) {
          return Text(state.time.toString());
        },
      ),
        floatingActionButton: FloatingActionButton(
          tooltip: 'Step',
          onPressed: () {  },
          child: const Icon(Icons.arrow_forward),
        )
    );
  }

With all this we get an appBar and button on the lower right of the screen:


Now, we need to fire an event when the button is pressed:

...        
        onPressed: () {
          context.read<C64Bloc>().add(C64Event());
        },
...
This is in effect the shorthand for getting our injected C64Bloc instance and adding an event to it when we press the button.

Now, with all our changes, lets do a test run. Upon startup we will see an initial timestamp. However, as we click the button, nothing happens. What is going on here?

Looking at the console, we see an exception is thrown every time, and looking closer, we see the error is thrown in our C64State class as highlighted here:

class C64State extends Equatable {
...
  @override
  // TODO: implement props
  List<Object?> get props => throw UnimplementedError();
}

Now this was part of the original code generated when I asked the IDE to implement the missing methods for me. Clearly, the IDE left me a TODO, which I ignored, but also purposefully ignored to illustrate a point 😁.

Lets unpack this a bit more. For starters, for what is the get props for? This is a method that need to be implemented when you extends Equatable.

Why do we need Equatable? This makes it easy to compare objects against its other. This is a common problem in many Object Oriented languages. How do we know if objects are equal? You can compare references, but that is a useless excercise since you will always different objects and it will not tell you if their values are equal.

Equatable comes to the rescue here, but you need to do something from your side. For get props you need to specify a list of property of your class that needs to be considered for comparison. In our case, we have just one property to add, time:

import 'package:equatable/equatable.dart';

class C64State extends Equatable {
  final int time = DateTime.now().millisecondsSinceEpoch;

  @override
  List<Object?> get props => [time];
}
If we now recompile, we see everything works as expected, and with any click, the timestamp updates.

I question might arise at this point: Why is a proper get props necessary for C64State, but not for C64Event? The answer is that it is always important for Flutter to know if a state has changed in order to trigger a redraw of your widget. This is where proper object comparison comes in.

In Summary

In post we continued our journey in creating a C64 emulator in Flutter. We explored basic concepts of Stateless widgets, state and events. These are all building blocks for working towards our emulator.

In the next post we will be implementing a couple of CPU instructions, and single step through a simple program, watching a snippet of memory and registers as we go along.

Until next time!

No comments:

Post a Comment