Getting started with Rust 🦀 2021: 7b. Building a GUI app in Rust [Part B]

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
all right so let's continue with where we left off in this part b of episode seven we're going to update headlines to fetch real world news data and also make it a bit more interactive by adding state object if you're new here we did part a of this episode in the previous video and the link is in the description [Music] so let's jot down the changes that we're gonna do in this video we'll start with adding a state object to headlines this will help us in persisting state across restarts we'll combine this with the confi and 30 crate to serialize the object in a config file and doing so would allow us to save the preferences of user across restarts with access to the state object we can then implement toggling between the dark and light theme by default egoi has dark mode enabled but it also has a set of styles for switching to the light theme we'll then create an api key config window to allow users to set the api key from within the app the api key will also be persisted across restarts using config finally we'll wire up the news api crate to fetch news articles within the scroll area widget with that being said i'm going to go back to headlines.harris and let's start adding the state object so i'm going to create a struct called headlines config and to start with we're going to add a field called dark mode and this is going to represent the state for the theme of our app if the value is true the theme will use the dark mode else it will use a wide theme so this is going to be a boolean and if you remember the button that we added within the render top panel for switching the theme this is the place where we'll be using the dark mode field to toggle between the two theme and if you notice that we capture the result of calling the add function with a button as a theme button variable and this is a response object and using the response object we can add event handlers on any widget that has been added to eq now our event handling strategy is very simple in this case we just need to capture a click event on the button for us to be able to toggle the theme so what we can do is we can type theme button.clicked and this is going to return a boolean so if the button is clicked we're going to modify the dark mode to be the inverse of the previous value that we had but if you see the error we don't have access to dark mode here so in order to have access to the dark mode field we're going to need to add the config in the headlines instance since render top panel is a method on headlines so let's do that i'm going to go back to headlines and i'm going to add in a field called config and this is going to be headlines config now since we have added this let's update the new method where this gets initialized i'm going to do config and let's call the new method this method is not created so i'm going to add in a new method or headlines config now if you don't know with an impul block we can use something called a self type alias to refer to the type that we are implementing the function verb and even for the initialization we can use self as the type alias and then we will keep dark mode as true by default so with the new method added let's go back to the render top panel function and this time we will have to use self dot config dot dark mode and also the same here and the compiler says that it is behind an immutable reference so the fix is simple we're just going to type and mute to convert this into a mutable reference and while we're still at the render top panel method let's also add an event handler to the close button as well so i'm gonna attach an event handler if the button is clicked we're gonna quit the app now the quit method is available on the frame instance so i'm gonna go back to main and we'll also pass this frame object from the update method to render top panel as well so i'm gonna pass it here and i'm gonna update the params in render top panel and let's update the quit method call to be part of the frame object let's do cargo run let's check if our theme button works and looks like there is not much happening how about the close button that works and the reason the theme switching does not work is that we're just changing the state here but are not acting on the value of the state so this can be done in the update method right before we render our widgets so what we can do is we can access the config object here and we'll check on the dark modes value and if it is true we're going to use the context object to set the visuals to be of dark theme else we're going to set this to a light theme and we're not able to access the config field because it is private so let's make it public let's also make it public for the dark mode field and let's also make this public cool so with that change we should be able to toggle between the dark and light theme and if you click on the theme button you can see that theme toggle works let's also change the icon depending on what theme is being set so back in headlines we can do a conditional expression here we'll replace this static string with a conditional expression and within this i'm going to check if self.config dot dark mode is enabled we're going to use a sun emoji otherwise it's going to be a moon and let's try that again and here we are able to change the icon as well based on the theme that we are in cool so let's also make some changes when the app has a light theme because if you take a look we're not able to read the title properly as well as the hyperlink which is barely readable in order to change that we're going to have to go back to the render newscards method and for the title of the article we're going to do the same conditional check so we can do something like if self dot config dot dark mode will add the title as white else we're going to add it as a black color now black isn't defined yet so i'm going to define the black color with all the components as zero and let's rename this to black let's also change the color of the hyperlink so if it's a dark mode we'll be rendering it as science else let's render it as red let's define red and we should see the new colors on the light theme and hey this is already looking a lot better now so for the refresh button we're gonna add it later once we integrate the news api crate next let's integrate the config crate so that we can save the app state in a file so i'm going to go to the headlines sub crate and i'm going to add comfy and i'm also going to add 30 and let's expand the import to also include the derive feature so for config to serialize a rust data structure it needs an annotation of service serialize and deserialize method so let's do that let's import 30 serialize and dc realize so then we can import the config object by using config's load function and we can give our config a name i'm gonna keep it headlines and this returns a result because for the first time this will fail so instead of unwrapping what we can do is we can use a method called unwrap our default and this needs a type annotation so let's do that let's put headlines config here and since we're using the unwrap our default method this means that in case the loading of config file fails this method will then try to call the default method implemented on the headlines config struct but since we haven't implemented the default trade on headlines config we're getting an error from the compiler so let's implement the default trade let's do impul default or headlines config and let's fill in the method and it's amazing that for some of the situations such as implementing defaults the rust analyzer is smart enough to auto implement the methods for us so in this case a boolean field already has the default method implemented so it just delegates the call to its implementation of default so with the default trait implemented we should be able to load the config file even if there isn't a config file in the file system now config uses the directories create underneath and in case of a linux system the configuration file will go to the dot config folder and the name of the configuration file that we gave which is headlines in this case and it follows a similar convention for other operating systems as well so with the config object being loaded from config we can then remove our previous initialization of headlines config and replace that with the one loaded from country and notice that we're using a shorthand syntax here or struct initialization which means that if the field and the variable names are the same you can simply type in the name and it will double up as an initializer so we now have the app state being persisted using config next we'll add an api key config window to allow users to set the api key which will then enable the app to load the news articles but before we do that let's set up a logger as it'll be helpful down the road unlike print line debugging which always requires one to recompile and rerun the program using a logger has an advantage to turn application logs on or off with a flick of an environment variable for the logger we'll use two crates the first is called tracing and then tracing subscriber tracing provides logging apis such as warn error or info as macros whereas tracing subscriber is an implementation of the subscribed trade the subscribe trade comes from the tracing crate and defines details such as how and where to output your logs in this case we're going to use the standard out subscriber which is called fmt so i'm going to initialize my subscriber in main.rs so initializing our subscriber is simple we can just to trace in subscriber and we'll use the fmt module and then call init once this has been initialized we can then use any one of the tracing apis for example error and can log anything on the screen so we'll be using this in later parts of the video and let's get back to creating the api config window under the input block for headlines i'm going to add a method called render config and this method will take in a context object so i'm going to pass that and within the method we'll then use a window widget which is again a layout container and use the new method to initialize this we can give this a name let's call it configuration and can then call the show method to render it on screen we'll pass the context object from above and for the second param this is going to be the closure and within the window let's first add a label let's put a helpful message so there's our label let's try to render this in the update method and right before the render top panel method we'll call render config and pass context object and let's make this public and let's try to run this and take a look at how things are looking nice as you can see we have a floating window being rendered on top of our app and right now this only contains a text label we're going to expand this and add a text input field so that users can enter their api key and configure the api key within the app so let's close that let's continue with render config let's add a text input label this can be done by using the text edit single line widget and as the text ram we need to pass a mutable reference to a string so that we can accept any input which has been entered by the user and to do that we can use a state variable that comes from the config struct we defined before i'm going to create a mutable reference to self dot dot key now the field api key is not defined so let's go back to our headlines config structure and i'm going to add an api key field here and this is going to be a string let's also update the initialization of the string this is going to be an empty string to start with and we have an error here and notice that we already have a default initializer for headlines config so we don't need the new method now so i'm going to delete this hole in the block and there is another error so let's go back to the error and notice the textedit single line method needs a mutable reference but since self is not borrowed as mutable you cannot create a mutable reference out of an immutable reference in rust so to fix that we're gonna have to pass self as mutable and this should work so with the text input added we can use it to enter the api key later we'll use the text input handle to the edit text widget to attach event handler so that we can collect the api key and save it using config but before that let's also add a bit of context so that the user knows where to get the api key from i'm going to quickly add a help text and with that let's try to run this again and hey we now have the text input field within the configuration window and we can type things here which will be taken up as the api key and just to make sure that this is actually being stored in self.config.api key field let's add a log line and see if you're able to capture the so i'm going to do tracing error and we'll use the format string and try to print the api key so let's run that and notice that we are already seeing a lot of log lines and the moment we tried typing in the text here you can see it is updating in real time so it's important to also note the fact that in egree the update method or the rendered loop renders many times in a second in egry by default it is around 60 fps so that's that so then let's try to attach an event handler for capturing the entered api key right now if i press enter this doesn't really do much since we don't have an input handler for the text edit widget so we're going to create an event handler and we'll need to combine two events here the first one is if the text input has lost focus and there has been an input where the event is a key press event for the key enter if we have both of these conditions satisfied we'll call the config crates store method use the headlines as the name of the config and then create a headlines config where we pass the dark mode as the current dark mode value and for the api key let's use the currently entered api key and this expression is trying to create a new string by value but it is a mutable reference so in order to fix that we can do two string and we will be able to store the newly created state object in config but this might return an error so let's handle the case where we could have an error so we can use the if let pattern match expression so i'm going to do if let error and we'll capture the error description here and if there is an error we'll then use facing's error macro to log the failure of saving the app state and then pass in the error description that we captured from here and just to see if our api key is being set we can simply log api key that has a string so then let's try to run this again and see if our event handler conditional works so i'm going to do cargo run and if i try to type in sequence of text and press enter you can see the api key set blog line so we are now able to store the app state by using config's store method and passing in a new headlines config object we can confirm this by going to the config folder in case of linux so if i try to print the contents of config headlines dot tom you can see that we have the state object serialized in a terminal file but if you notice even though we have our api key set now the configuration window still stays at the top and ideally we want this window to go away and load our articles list so either we would have the api key config window shown if the api key is not set or else we should get a list of news articles widget being rendered in the app so let's make this exclusive let's add a check so that we only render one of the uis based on whether the api key is set or not so we can use a simple boolean flag here and this is going to be per session so i'm going to add a field called api key initialized which is going to be a boolean let's initialize this as false and using this field we can conditionally render either the render config window or the remaining widget in the app what i'm going to do is i'm going to conditionally check if self.api key is not initialized we'll do self.renderconfig else will render the remaining of the ui now api key is private so let's make it public and let's try to run this and as expected we're only seeing the api config window being rendered in the app so this is the ideal behavior we want and additionally if the user enters their api key and presses enter the config window should go away and it should render this part of the ui which is the list of articles so let's add that behavior we can go to the event handler for for the api key text input and right after we save the new app state we'll also set the api key initialize field to true and with that change pressing enter on the api key should render our list of articles so then it's time to finally start integrating the news api crate so that we can render real news articles within rendered news card smith so i'm going to add the news api create in cargo.tunnel and since this is a local crate i'm going to provide its path and then let's go back to main.rs and we'll create a helper function called patch news so within fetch news let's initialize the news api instance with the new function we'll pass in the api key and then we'll call fetch and this returns a result so let's match on the ok variant of the result we'll capture it as a response and then using response we'll grab the list of articles we can then iterate over the list of articles and map each element as a newscard data instance but the title will grab the title for the euro we do the same for the description we'll use the desk method and this is an optional so in case the value of description is a none we can use the unwrap or method to compute a dummy description so i'm going to put three dots here and let's convert that to a string and notice we still have an error because the underlying type returned by the description function is a reference to our strings so there needs to be another step where we map the internal value to a new copy of the string and this should work so we are now fetching the actual data from the top headlines endpoint and converting all of them into newscard data but we need a way to update the articles field in headlines with the data that we have received in fetch news and we also need to call this method the moment the app starts and a good place to call this function is in the setup method so let's call fetch news here and we're going to pass the api key from the config object and let's make it a reference and to update the articles field in headlines let's also pass a mutable reference to the self.articles field and let's update the fetch news param and this is going to be an mute of vector of news card data and then let's update the code to also push the news instance that we created before it looks like we have an error and this error is because there is a name clash happening so so we have articles here and we also have articles being redefined here so this articles is being shadowed by the variable created on line number nine so i'm gonna rename this to something else let's call it response articles let's update the usage as well and and this should work so then let's try to run this and see if we are able to fetch the news data and looks like even though we we've already set up the api key this window doesn't go away ideally we want the config window to go away if the api key has been set so let's fix that i'm going to go back to headlines and the place where we instantiate the api key initialized field this should now be changed to check if the api key has some value instead of being false all the time so what we can do is we can do config dot api key and we can check if the value is not empty and we have an error from the compilers saying that the config instance is already used as a value for the headlines instance so we cannot use this so we can do two things either we can create a copy of this boolean value before we assign the config to headlines or we can change the order of field initialization which would remove the error in a simple manner so let's try to run this and we should see the config window go away and that works but if you notice the first few seconds the app start where we had a couple of seconds of black screen that was the point where the app was lagging and trying to make network requests to news api and if i scroll down you will see we have the news articles from the top headlines endpoint so this works but we have a lag and we also have these dummy list of articles that we should remove so let's fix the lag issue and let's also remove the dummy list of articles so i'm going to simply remove this line and we should no longer see dummy news articles and let's update this to just be an empty vector we're going to initialize this as part of fetch news method so let's run this again and notice the lag again and after a few seconds we should see the real news articles so our app is pretty much functional here we can click on the links and this will open up in the browser but we still have that initial blank screen and quite a bit of lag during the initial startup so so let's fix that and this is the part where we get to use rust threads and channels so let's close that i'm going to go back to main.rs the reason we have an initial lag in the app is because by making a blocking network request in the main ui thread we're blocking agree from rendering anything on the screen so only after the fetch news function returns we are then able to see the screen render to do this in a better way we need a way to offload the fetching of articles into a separate thread so what we can do is we can create a thread here we can spawn a thread from the std thread module we'll use the spawn function and this takes in a closure it's an empty closure with no params and within the body we'll then move our fetch method from news api to fetch the news articles now we have an error here and it says something that the static lifetime is required on articles and api key and this error message can be a bit confusing if you're new to rust we'll have a series on the basics of rust where we'll go over lifetimes in detail and everything will make sense after that but just to give you a brief overview think of the text static as a validity tag for reference types which denotes that the reference lives for the whole lifetime of the program to be specific the value being referenced must live in the data section of the binary for us to be able to create a static reference out of it but that is clearly not the case with api key and articles here since they are created back in main so if i go back to the call site you will see that the fetch news is called from the setup method and setup is implemented on headlines and headlines is initialized back in main and because threads can outlive their parent thread for instance if they are launched as daemon threads so that's why the compiler is unhappy about that so an obvious fix for the lifetime issues in rust is to create a copy of the type we can do that for the api key that's going to be simple so i'm going to do api key dot clone and we have one error down but copying won't work for articles here since we would end up not modifying the original article fields in a headline but instead we'll be making changes to the local copy of articles so another solution is to wrap articles in a mutex and share the variable within this thread but this would involve making significant changes in the type definition of headlines so the third alternative approach is to use rust channels which has a much nicer api rust channels underneath are basically right safe cues but they provide us with two handles to the queue one is called the sender instance and the other is called the receiver so the usual pattern of using channels is that you can pass the sender to as many threads as you want and for the receiver you can use that to receive messages from n number of threads so we're going to use the exact pattern here so i'm going to create a channel here and calling channel will return us a tuple of two elements the first is called the center half of the channel and the second is called the receiver half of the channel we're going to call the sender news tx and the receiver news rx tx and rx are just naming conventions that is quite common in rust projects using channels so with the sender and receiver created we can then use the sender side within the thread and instead of pushing the news and articles we'll do news tx dot send and we're gonna send the news article and notice that we have an error here and this error means that the closure is trying to take the sender as a reference but instead we are okay to pass news tx as a value so we can explicitly ask the closure to take ownership of any values that gets used within the closure with the move keyword and looks like there's still an error for the api key that we have so instead of calling clone we should be calling the tostring method we should be calling the tostring method here and the error should go away and this needs a reference let's make it a reference here and this should fix all of our errors i'm going to remove this now calling send might give us an error so it returns result type in case the receiver side of the handle is deallocated or dropped so let's check for any errors that we might possibly get from sending the news article and let's log this using tracing's error macro let's put a message so that's our sender side of things done but it is of no use if we can't read data from the receiver side so what we'll have to do is we'll need to attach the receiver side as an instance of headlines so that we can use that in the update method to read news articles from it so we'll be creating a field called newsrx on headlines and we will assign that to the news rx we have created here let's add a field in headlines and this is going to be a news rx and it is of type receiver and the received type is going to be news card data let's go back now we're trying to use self here but this function doesn't have access to self so let's move all of this inside setup and let's remove the fetch news function and this as well this will then become self.config.api key and let's fix the name of the field and we should be able to assign the value to news rx now this is going to be an optional type since we won't have an rx created right at the initialization it is only going to be assigned the moment we spawn a child thread and that will happen only in the setup method so that's that and let's also update the initialization of the headlines instance in new with the news rx since we won't have an instance of the receiver so we can initialize that with none to start with so with that change our app should be sending news articles on a channel and the only thing now left is to receive our messages back in the update method so to receive the articles sent from this child thread back in update let's create a method called self dot reload articles and this is going to be a method on headlines within this method we're going to try to receive the news articles from the news rx receiver end so i'm going to do self.news rx and we're going to match if we have a news rx instance and we're going to match again on the inner type and we are going to call by receive on it now the try receive method is a non-blocking call which means that in case there isn't anything on the channel it will not block the thread but instead we'll return from the column the calling try receive can give us either an okay value with our news data and if it is a news data that we have received we will then push the news card data into self dot articles otherwise if it's an error we'll just log this error using tracing's warn api and we have another error we simply need to prefix this with ampersand and this should fix that so let's try to run this now and see if we are able to fetch the news articles and this already looks a lot close to what we had aimed for but if you notice a little weird behavior there that if i try to run this app again notice that we're not seeing anything being rendered yet but the moment i move the cursor to the screen you can see that it will start to populate the list of articles and this happens because egoy by default tries to be as efficient as possible in terms of resource consumption so it will only render the widget state as long as it sees any activity on the app but if you don't want that behavior we can explicitly ask eagwe to render the article as soon as there is any change in the state so let's do that so right before we do any rendering of the widget we can use the context object and call the requestpaint method what this will do is regardless of any input or widget update that has happened egre will try to render reframe as needed so if you run this now we'll see that our app floats immediately without us having to move the cursor on the screen so we're almost done here but there is one edge case that we haven't accounted for remember that our app loads and tries to read the config file and then proceeds to fetch the articles now if someone were to launch this app for the first time they would not be having the api keys set and our app will break so let's simulate that case i'm going to delete the headlines config and i'm going to run our app again and as expected we will see the api config window so let's enter our api key and press enter and notice that the articles are not being loaded in this case and that's because by the time the render config window appears the thread has already spawned and was trying to fetch the news articles with an empty api key since it starts as an empty string and so it fails and then the thread exits immediately so to fix that we'll need to make the thread wait and be aware of quote unquote the first time launch state of the app and we can do that by checking for the api key within the thread so if the api key is not empty we'll call a helper function called patch news to this we will pass the api key and a mutable reference to the news tx sender this method isn't created so let's create that i'm going to name this fetch news let's pass the api key as a reference to a string and this is also going to be a mutable reference and this is not mutable so let's make this mutable so if the api key is not empty we go the usual path of fetching the news articles otherwise we will go into a loop and here we'll try to wait for a message from the update method and for that let's create a new channel here and this is going to be a sync channel with a size of one since we're just going to be using it as a signaling mechanism so this also returns us a sender and receiver pair i'm going to name this app.tx and the other one as app rx and for the sync channel the type of message that we're going to send is going to be a message enum so let's define that i'm going to create a name called message and i'm going to add a variant called api key set and this is going to carry a string which will be the api key and since we're going to be sending this message from the update method so let's add a field in the headlines struct let's call it apptx and this is also going to be an option and it is going to be a sync sender type and it will be sending data of type message and let's update the initialization for aptx as none so then using the app.tx sender the moment we press the enter key on the api config window we will also send a message on the aptx channel so we'll check if we have the app tx and if there is a sender we'll then send the message which will be of type api key and this is going to contain the newly set api key as a string so now that we are signaling the event of setting the api key in the config window we can then act on this message in the else block here within the loop so what we can do here is we can match on app rx and call receive on it and if we receive an ok message of type api key set with the api key we will then call fetch news with the api key and the sender and let's make the message type public so we can use it and for this let's pass our reference and there is an error on the receive method call for not covering the error variant so let's catch the error variant as well and let's log the error and we have one more error so let's fix that and this should work now and there's one small part that we need to add which is to assign the app.tx to the aptx that we have created here so then i'm going to remove the headlines config and we're going to start fresh i'm going to do cargo run and let's enter the api key and this time we should see the articles being rendered on the screen so we're pretty much done here and i'm going to leave the implementation for the refresh button here as an exercise for you as it will help me understand that you're able to follow what i explained so feel free to fork the repository and implement the refresh button and you can share your solutions in the comments with the link to your repo so with that we have come to the end of this video and i hope this was helpful and i'll see you in the next one
Info
Channel: creativcoder
Views: 2,834
Rating: undefined out of 5
Keywords:
Id: SvFPdgGwzTQ
Channel Id: undefined
Length: 36min 19sec (2179 seconds)
Published: Tue Oct 19 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.