Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Create a Flashcard Android app with Jetpack Compose

Afzal Najam
January 14, 2023

Create a Flashcard Android app with Jetpack Compose

Presentation for DeltaHacks 9 at McMaster University on Jan 14, 2023

Afzal Najam

January 14, 2023
Tweet

More Decks by Afzal Najam

Other Decks in Education

Transcript

  1. This work is licensed under the Apache 2.0 License
    Create a Flashcard App
    With Jetpack Compose

    View Slide

  2. This work is licensed under the Apache 2.0 License
    ● Afzal Najam
    ● Android Developer since 2014
    ● Working at Doist
    Who am I?

    View Slide

  3. This work is licensed under the Apache 2.0 License
    ● Basic computer literacy
    ● Basic math skills
    ● Computer
    ● Internet connection
    ● (Optional) Android device & USB cable
    Prerequisites
    Here are some the prerequisites that will be helpful. Having basic computer literacy
    and basic math skills is recommended. You’ll also need a computer and access to the
    internet to take the online course. [Mention WiFi instructions if necessary.]

    View Slide

  4. This work is licensed under the Apache 2.0 License
    ● Build your first Android apps
    ● Learn the basics of the Kotlin programming language
    ● Learn Jetpack Compose
    ● Discover resources to continue learning
    Learning Objectives

    View Slide

  5. This work is licensed under the Apache 2.0 License
    Kotlin Programming
    Language
    Use Kotlin to start writing Android apps.
    Kotlin helps developers be more
    productive.
    In this workshop, you’ll learn how to create Android app UI using Kotlin and Jetpack
    Compose. We’ll talk about Jetpack Compose in a minute but first, Kotlin. Kotlin is a
    programming language recommended by Google for creating new Android apps. It’s a
    modern and popular programming language, known for helping developers be more
    productive and more concise when writing code.
    As a result of many great language features, Kotlin has quickly gained momentum in
    industry and is used by over 50% of professional Android developers.
    Let’s check out the basics of Kotlin.
    [Read about Android’s Kotlin-first approach]

    View Slide

  6. This work is licensed under the Apache 2.0 License
    Kotlin Playground
    Write and run Kotlin code in
    the browser.
    https://bit.ly/kotlin-pg
    To make it easier for you to learn, you’ll be writing your code in the Kotlin Playground
    which you can access via the web browser. The site looks something like this. You
    can write your code in this window and run it by hitting the green Run button. The
    result of your code (known as the output) will show up at the bottom of the window
    (where it says “Hello, world!”).
    To illustrate a few important concepts that you’ll learn in this workshop, we will go
    through a short code demo to create a program in Kotlin.

    View Slide

  7. This work is licensed under the Apache 2.0 License
    main Function
    The main function is the entry
    point, or starting point, of the
    program.
    Start here
    fun main() {
    println("Hello, world!")
    }
    Output:
    Hello, world!
    A Kotlin program is required to have a main function, which is the entry point, or
    starting point, of the program.
    How many people here don’t know what a function is?

    View Slide

  8. This work is licensed under the Apache 2.0 License
    Functions
    A function is a segment of a program that
    performs a specific task.
    You can have many functions in your program or
    only a single one.
    A function is a segment of a program that performs a specific task. You can have
    many functions in your program or only a single one.
    Creating separate functions for specific tasks has a number of benefits.
    ● Reusable code: Rather than copying and pasting code that you need to use
    more than once, you can simply call a function wherever needed.
    ● Readability: Ensuring functions do one and only one specific task helps other
    developers and teammates, as well as your future self to know exactly what a
    piece of code does.

    View Slide

  9. This work is licensed under the Apache 2.0 License
    Defining a function
    Functions begin with the fun
    keyword.
    fun displayIntroduction() {
    }
    We will demonstrate how to define a function with a function called
    displayIntroduction() that we will use to print our name and age.
    A function definition in Kotlin starts with the fun keyword. A keyword is a reserved
    word that has a special meaning in Kotlin, in this case the fun keyword tells Kotlin
    that you are going to make a function.

    View Slide

  10. This work is licensed under the Apache 2.0 License
    Defining a function
    Functions have a name so that
    they can be called.
    fun displayIntroduction() {
    }
    Functions need to have a descriptive name so that they can be called from other parts
    of the program.

    View Slide

  11. This work is licensed under the Apache 2.0 License
    Defining a function
    Functions need a set of parentheses
    after the function name in order to
    surround the function inputs.
    fun displayIntroduction() {
    }
    Functions need a set of parentheses which you can use to optionally pass information
    into the function. displayIntroduction() won’t need information passed in. You
    will learn more about passing in inputs when we talk about Jetpack Compose.

    View Slide

  12. This work is licensed under the Apache 2.0 License
    Defining a function
    The curly braces make up the
    function body and contain the
    instructions needed to execute
    a task.
    fun displayIntroduction() {
    }
    Functions need curly braces that contain the instructions needed to execute a task.

    View Slide

  13. This work is licensed under the Apache 2.0 License
    Putting it together
    fun displayIntroduction() {
    // We will fill this out!
    }
    Output:
    Hi I’m Meghan and I am 28 years old
    The task of the displayIntroduction() function, is to print your name and age.
    In order to do that you will save both your name and age into variables.

    View Slide

  14. This work is licensed under the Apache 2.0 License
    Basic data types
    Kotlin Data type What kind of data it can contain Example literal values
    String Text
    “Add contact”
    “Search”
    Int Whole integer number
    32
    -59873
    Double Decimal number
    2.0
    -37123.9999
    Float
    Decimal number (less precise than a Double).
    Has an f or F at the end of the number.
    5.0f
    -1630.209f
    Boolean
    true or false. Use this data type when there
    are only two possible values.
    true
    false
    When you decide what aspects of your app can be variables, it's important to specify
    what type of data can be stored in those variables. In Kotlin, there are some common
    basic data types. This table shows a different data type in each row. For each data
    type, there's a description of what kind of data it can hold and example values.
    A String holds text so you will use it to store your name, and an Int holds an
    integer number so you will use it to store your age.

    View Slide

  15. This work is licensed under the Apache 2.0 License
    val keyword
    Use when you expect the variable value will
    not change.
    Example: name
    var keyword
    Use when you expect the variable value can
    change.
    Example: age
    Defining a variable
    Now, let’s jump into how you define a variable.
    You can declare a variable using either val or var.
    With val, the variable is read-only, which means you can only read, or access, the
    value of the variable. Once the value is set, you cannot edit or modify its value.
    With var, the variable is mutable, which means the value can be changed or
    modified. The value can be mutated.
    In Kotlin, it's preferred to use val over var when possible.
    We will store your name as a val because that will not change.
    We will store your age as a var because it changes every year.

    View Slide

  16. This work is licensed under the Apache 2.0 License
    Defining a variable
    Variables start with a var or val
    keyword.
    fun displayIntroduction() {
    val name: String = "Meghan"
    var age: Int = 28
    }
    To demonstrate how to define a variable we will define both name and age variables.
    Before you use a variable, you must declare it. To declare a variable, start with the
    val or var keyword.

    View Slide

  17. This work is licensed under the Apache 2.0 License
    Defining a variable
    All variables must have a name.
    fun displayIntroduction() {
    val name: String = "Meghan"
    var age: Int = 28
    }
    All variables must have a name that they can be referenced by.

    View Slide

  18. This work is licensed under the Apache 2.0 License
    Defining a variable
    Data type is the type of data
    that the variable holds.
    fun displayIntroduction() {
    val name: String = "Meghan"
    var age: Int = 28
    }
    The data type specifies the type of data that the variable holds. Note that a colon
    separates the name and data type.

    View Slide

  19. This work is licensed under the Apache 2.0 License
    Defining a variable
    The initial value is the value that
    is stored in the variable.
    fun displayIntroduction() {
    val name: String = "Meghan"
    var age: Int = 28
    }
    In the variable declaration, the equal sign symbol (=) follows the data type. The equal
    sign symbol is called the assignment operator. The assignment operator assigns a
    value to the variable. The variable’s initial value is the data stored in the variable.

    View Slide

  20. This work is licensed under the Apache 2.0 License
    Putting it together
    fun displayIntroduction() {
    val name = "Meghan"
    val age = 28
    println("Hi I'm $name and I am $age years old")
    }
    Let’s finish putting the displayIntroduction() function together. We have our
    variables but they don’t do anything yet.
    Let’s add a print statement to print out your introduction using println to print to the
    output in Kotlin Playground.
    In order to print your variables, you will use String templates which allow you to
    include variable references in a string by using the $ sign before the variable name.
    [You can learn more about String Templates here]

    View Slide

  21. This work is licensed under the Apache 2.0 License
    Putting it together
    fun main() {
    displayIntroduction()
    }
    fun displayIntroduction() {
    val name = "Meghan"
    val age = 28
    println("Hi I'm $name and I am $age years old")
    }
    Output:
    Hi I’m Meghan and I am 28 years old
    Finally, we will replace the contents of the main() function with a call to the
    displayIntroduction() function when we run it, “Hi I’m Meghan and I am 28
    years old” will print to the output.

    View Slide

  22. This work is licensed under the Apache 2.0 License
    Higher-order functions
    fun main() {
    displayIntroduction({ it -> println(it) })
    }
    fun displayIntroduction(block: (String) -> Unit) {
    val name = "Meghan"
    val age = 28
    block("Hi I'm $name and I am $age years old")
    }
    Output:
    Hi I’m Meghan and I am 28 years old
    There’s one more concept called Higher-order functions. Kotlin allows us to pass
    functions as arguments to other functions. This is very useful to run code that needs
    run later, for example code that would run in response to a user click.
    Here, instead of calling “println” in the displayIntroduction(), we will call the function
    passed to displayIntroduction() and provide it the String argument. Because we’ve
    passed a function that calls println, when we call block(), that function will execute.

    View Slide

  23. This work is licensed under the Apache 2.0 License
    Higher-order functions
    fun main() {
    displayIntroduction { it -> println(it) }
    }
    fun displayIntroduction(block: (String) -> Unit) {
    val name = "Meghan"
    val age = 28
    block("Hi I'm $name and I am $age years old")
    }
    Output:
    Hi I’m Meghan and I am 28 years old
    We can also omit the parentheses when calling the function when the last argument is
    a function.

    View Slide

  24. This work is licensed under the Apache 2.0 License
    Jetpack Compose
    bit.ly/android-dh
    bit.ly/android-dh-zip
    Jetpack Compose is a modern toolkit for building Android UIs. Compose simplifies
    and accelerates UI development on Android with less code, powerful tools, and
    intuitive Kotlin capabilities. With Compose, you can build your UI by defining a set of
    functions, called composable functions, that take in data and emit UI elements.
    To illustrate a few important concepts that you’ll learn in this pathway, we will go
    through a short code demo to create a flashcard app in compose.

    View Slide

  25. This work is licensed under the Apache 2.0 License
    A composable function
    ● Describes some part of your UI.
    ● Doesn't return anything.
    ● Takes some input and generates what's shown on the screen.
    ● Might emit several UI elements.
    To start, let’s talk about Composable functions. Composable functions are the basic
    building block of a UI in Compose. A Composable function:
    ● Describes some part of your UI.
    ● Doesn't return anything.
    ● Takes some input and generates what's shown on the screen.
    ● Might emit several UI elements.

    View Slide

  26. This work is licensed under the Apache 2.0 License
    Greeting()
    @Composable
    fun Greeting(name: String) {
    Text(text = "Hello $name!")
    }
    To see the parts of a Composable function, let’s take a look at the Greeting()
    function that is in MainActivity when you create a new Compose project.

    View Slide

  27. This work is licensed under the Apache 2.0 License
    Greeting()
    @Composable
    fun Greeting(name: String) {
    Text(text = "Hello $name!")
    }
    All Composable functions must have the Composable annotation which informs the
    Compose compiler that this is a Composable function. If you forget to add this
    annotation, your code will not compile! An annotation is applied by prefixing its name
    (the annotation) with the @ character at the beginning of the declaration you are
    annotating. Annotations are means of attaching extra information to code. This
    information helps tools like the Jetpack Compose compiler understand the app's
    code.

    View Slide

  28. This work is licensed under the Apache 2.0 License
    Greeting()
    @Composable
    fun Greeting(name: String) {
    Text(text = "Hello $name!")
    }
    You use the fun keyword to denote to the Kotlin compiler that it is a function.

    View Slide

  29. This work is licensed under the Apache 2.0 License
    Greeting()
    @Composable
    fun Greeting(name: String) {
    Text(text = "Hello $name!")
    }
    In Compose, function names are capitalized. Note that this is only in Compose, a non
    Compose function should start with a lowercase letter.

    View Slide

  30. This work is licensed under the Apache 2.0 License
    Greeting()
    @Composable
    fun Greeting(name: String) {
    Text(text = "Hello $name!")
    }
    Composable functions can accept parameters. When the value of those parameters
    change, the displayed UI will change as well.

    View Slide

  31. This work is licensed under the Apache 2.0 License
    Greeting()
    @Composable
    fun Greeting(name: String) {
    Text(text = "Hello $name!")
    }
    Composable functions don't return anything. Since Composable functions only
    describe the UI, they don't construct or create the UI, so there is nothing to return.

    View Slide

  32. This work is licensed under the Apache 2.0 License
    Flashcard App
    For our example app we will be creating a flashcard app that shows a term, when you
    tap it, it changes to show the definition. Then you can swipe to go to the next card.
    We will build the app piece by piece but at the end it will look like this!

    View Slide

  33. This work is licensed under the Apache 2.0 License
    Term()
    @Composable
    fun Term() {
    }
    We will start by creating a new Composable function called Term().

    View Slide

  34. This work is licensed under the Apache 2.0 License
    PetName()
    @Composable
    fun Term() {
    Text()
    }
    Inside of the function body we will add a Text() Composable.

    View Slide

  35. This work is licensed under the Apache 2.0 License
    PetName()
    @Composable
    fun Term() {
    Text(text = "Computer")
    }
    In the Text() Composable we will use the named argument, text, and set it to a
    String saying: “Computer”. Feel free to replace the text with another term if you want.

    View Slide

  36. This work is licensed under the Apache 2.0 License
    DefaultPreview()
    @Preview(showBackground = true)
    @Composable
    fun DefaultPreview() {
    FlashcardTheme {
    Term()
    }
    }
    In order to see the contents of Term() add it into the DefaultPreview().

    View Slide

  37. This work is licensed under the Apache 2.0 License
    DefaultPreview()
    This is how Term() shows up in the Design pane. Looks good!

    View Slide

  38. This work is licensed under the Apache 2.0 License
    DefaultPreview()
    @Composable
    fun FlashcardApp() {
    //...
    FlashcardTheme {
    Surface(...) {
    Term()
    }
    }
    }
    To include it as part of the app to run on a device or emulator, let’s add Term to the
    FlashcardApp Composable in place of Greeting()

    View Slide

  39. This work is licensed under the Apache 2.0 License
    Definition()
    @Composable
    fun Definition() {
    Text(text = "a programmable usually electronic device
    that can store, retrieve, and process data")
    }
    To display the definition of a term, we will make another Composable called
    Definition() and displays the definition. Set the text equal to “a programmable
    usually electronic device that can store, retrieve, and process data”.

    View Slide

  40. This work is licensed under the Apache 2.0 License
    Column and Row
    Column
    Row
    Now we have two Composables that we want to display. Column and Row are two
    layout Composables that are used in Compose to arrange multiple Composables.

    View Slide

  41. This work is licensed under the Apache 2.0 License
    DefaultPreview()
    For this app we will use a Column to arrange the information vertically with Term()
    appearing before Definition().

    View Slide

  42. This work is licensed under the Apache 2.0 License
    FlashcardItem()
    @Composable
    fun FlashcardItem() {
    Column {
    }
    }
    In order to add a Column to the app add a new Composable called
    FlashcardItem() and add a Column Composable inside of it.

    View Slide

  43. This work is licensed under the Apache 2.0 License
    FlashcardItem()
    @Composable
    fun FlashcardItem() {
    Column {
    Term()
    Definition()
    }
    }
    Next, add both Composables to the Column. Add Term() before Definition() so
    that it displays first.

    View Slide

  44. This work is licensed under the Apache 2.0 License
    DefaultPreview()
    @Preview(showBackground = true)
    @Composable
    fun DefaultPreview() {
    FlashcardTheme {
    FlashcardItem()
    }
    }
    To view the new Column you added, add FlashcardItem() to the
    DefaultPreview().

    View Slide

  45. This work is licensed under the Apache 2.0 License
    DefaultPreview()
    This is what the DefaultPreview() looks like with the Column showing. Notice
    how the Term() shows up first since we added it to the Column first.

    View Slide

  46. This work is licensed under the Apache 2.0 License
    FlashcardItem()
    @Composable
    fun FlashcardItem() {
    Column {
    Term()
    Definition()
    }
    }
    Right now, both the term and definition are shown at the same time but we only want
    to show one of them and then show the other when the card is tapped, so we need to
    keep some sort of state. When the card is clicked, the state will change to reveal the
    definition.

    View Slide

  47. This work is licensed under the Apache 2.0 License
    FlashcardItem()
    @Composable
    fun FlashcardItem() {
    var showDefinition = false
    Column {
    Term()
    Definition()
    }
    }
    To do this. First, we’ll create a variable called “showDefinition” and initialize it to false.

    View Slide

  48. This work is licensed under the Apache 2.0 License
    FlashcardItem()
    @Composable
    fun FlashcardItem() {
    var showDefinition = false
    Column(Modifier.clickable {
    showDefinition = !showDefinition
    }) {
    // ...
    }
    }
    And then we make Column clickable so that clicking it changes the value of
    showDefinition.

    View Slide

  49. This work is licensed under the Apache 2.0 License
    FlashcardItem()
    @Composable
    fun FlashcardItem() {
    var showDefinition = false
    Column(Modifier.clickable { showDefinition = !showDefinition }) {
    if (!showDefinition) {
    Term()
    } else {
    Definition()
    }
    }
    }
    Then we can do this. However, this won’t work as expected. Setting a different value
    for showDefinition variable won’t make Compose detect it as a state change, so
    nothing will happen.
    The reason for that is that it’s not being tracked by Compose. Compose apps
    transform data into UI by calling Composable functions. When your data changes,
    Compose re-executes these functions with the new data to update the UI. This is
    called recomposition. Compose also looks at what data has changed to determine
    which Composable to call so that it only recomposes components whose data has
    changed, and skips recomposition for components whose data hasn’t changed.
    So each time FlashcardItem is called (or recomposed), the value of showDefinition
    will reset to false.

    View Slide

  50. This work is licensed under the Apache 2.0 License
    FlashcardItem()
    @Composable
    fun FlashcardItem() {
    var showDefinition = mutableStateOf(false)
    // ...
    }
    To add internal state, you can use the mutableStateOf function. If this were done
    outside of a Composable function, it would be retained. However, in a Composable
    function, we have the problem where it will be called many times during
    recomposition, which would reset this.

    View Slide

  51. This work is licensed under the Apache 2.0 License
    FlashcardItem()
    @Composable
    fun FlashcardItem() {
    var showDefinition = remember { mutableStateOf(false) }
    // ...
    }
    To preserve states across recompositions, we tell Compose to remember the mutable
    state by using the remember function.

    View Slide

  52. This work is licensed under the Apache 2.0 License
    FlashcardItem()
    @Composable
    fun FlashcardItem() {
    val showDefinition = remember { mutableStateOf(false) }
    Column(Modifier.clickable { showDefinition.value = !showDefinition.value }) {
    if (!showDefinition.value) {
    Term()
    } else {
    Definition()
    }
    }
    }
    Because we’re storing the state in a MutableState object, we need to modify our code
    to access the value inside it.
    If we run this code now, we can tap on the card item, it will mutate showDefinition,
    and Compose will recompose this component to reflect the change by showing
    Definition() instead of Term().

    View Slide

  53. This work is licensed under the Apache 2.0 License
    Let’s build and refresh the preview, and also start interactive mode to see what
    clicking the card does.

    View Slide

  54. This work is licensed under the Apache 2.0 License
    Now when we tap on the card in the preview, it changes the text to definition.

    View Slide

  55. This work is licensed under the Apache 2.0 License
    FlashcardItem()
    @Composable
    fun FlashcardItem() {
    Card {
    Column {
    // ...
    }
    }
    Now, this doesn’t look like a card still. In fact, if you run it on an emulator or device
    and tap, the tap area only spans as far as the text. Let’s change that, after all, it is
    called a “flash card”.
    Let’s wrap the Column in a Card composable.

    View Slide

  56. This work is licensed under the Apache 2.0 License
    FlashcardItem()
    @Composable
    fun FlashcardItem() {
    Card {
    Column(Modifier
    .clickable { }
    .height(160.dp)
    .width(320.dp)
    ) {
    // ...
    }
    }

    View Slide

  57. This work is licensed under the Apache 2.0 License
    FlashcardItem()
    @Composable
    fun FlashcardItem() {
    Card {
    Column(Modifier
    .clickable { }
    .height(160.dp)
    .width(320.dp)
    ) {
    // ...
    }
    }
    It’s bigger now but things aren’t centered. Luckily, Column (and Row or even Box for
    that matter) provide vertical and horizontal alignment arguments.

    View Slide

  58. This work is licensed under the Apache 2.0 License
    FlashcardItem()
    @Composable
    fun FlashcardItem() {
    Card {
    Column(
    verticalArrangement = Arrangement.Center,
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier
    .clickable { //.. }
    .height(160.dp)
    .width(320.dp)
    ) {
    // ...
    }
    }
    It’s bigger now but things aren’t centered. Luckily, Column (and Row or even Box for
    that matter) provide vertical and horizontal alignment arguments.
    To specify other arguments for Column, let’s use the named arguments feature in
    Kotlin.

    View Slide

  59. This work is licensed under the Apache 2.0 License
    Everything is looks nice and centered!

    View Slide

  60. This work is licensed under the Apache 2.0 License
    FlashcardItem()
    @Composable
    fun FlashcardApp() {
    val viewModel by viewModels()
    val flashcardDataList = viewModel.flashcardDataList
    FlashcardTheme {
    Surface(...) {
    FlashcardItem()
    }
    }
    }
    Now, we’re only showing one flashcard here. Which is okay but it’d be nice if we could
    swipe through to other questions.
    In the app, you’ll find another file called FlashcardViewModel that contains a
    flashcardDataList. We can use this list to populate a pager with multiple cards and
    swipe through them.
    You can already see that we have a flashcardDataList that we’re not using inside
    FlashcardApp

    View Slide

  61. This work is licensed under the Apache 2.0 License
    FlashcardItem()
    @Composable
    fun FlashcardApp() {
    val viewModel by viewModels()
    val flashcardDataList = viewModel.flashcardDataList
    FlashcardTheme {
    Surface(...) {
    HorizontalPager(count = flashcardDataList.size) { page ->
    val flashcardData = flashcardDataList[page]
    FlashcardItem(flashcardData)
    }
    }
    }
    }
    Just like Column and Row and Card, there’s another component called
    HorizontalPager. Let’s wrap the FlashcardItem within this Composable.

    View Slide

  62. This work is licensed under the Apache 2.0 License
    @Composable
    fun FlashcardItem(flashcardData: FlashcardData) {
    // ...
    Term(flashcardData.term)
    // ...
    Definition(flashcardData.definition)
    // ...
    }
    FlashcardItem()

    View Slide

  63. This work is licensed under the Apache 2.0 License
    @Composable
    fun Term(term: String) {
    Text(term)
    }
    @Composable
    fun Definition(definition: String) {
    Text(definition)
    }
    FlashcardItem()

    View Slide

  64. This work is licensed under the Apache 2.0 License
    Android Basics
    with Compose Course
    You’ll be working through the Android Basics with Compose course. This is an online
    training course developed by Google for people who want to learn how to build basic
    Android apps.

    View Slide

  65. This work is licensed under the Apache 2.0 License
    g.co/android/basics-compose
    Start here:
    Open the course page with the link on screen.

    View Slide

  66. This work is licensed under the Apache 2.0 License
    Then, click Unit 1 to start the Android Basics course.

    View Slide

  67. This work is licensed under the Apache 2.0 License
    Have a Question?

    View Slide

  68. This work is licensed under the Apache 2.0 License
    Resources
    ● Official Android Developers Site: developer.android.com
    ● Official Android Developers Blog (for announcements)
    ● Android Developers Medium Blog (for more technical articles)
    ● Android Developers YouTube channel
    ● Follow @AndroidDev on Twitter
    ● Follow @AndroidDev on LinkedIn
    ● Subscribe to the Android Developer Newsletter
    ou can check out these additional resources, which professional developers use to
    stay up to date on Android. As you get into more advanced features, you will likely
    need to learn more programming concepts. You can check out the Learn Kotlin By
    Example or the Kotlin language website resources for that.

    View Slide