├── 05. Form ├── images │ ├── form.png │ └── controller.png └── README.md ├── 06. CRUD Local Database ├── images │ └── hasil.gif ├── crud_local_database_app │ ├── android │ │ ├── gradle.properties │ │ ├── app │ │ │ ├── src │ │ │ │ ├── main │ │ │ │ │ ├── res │ │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── drawable │ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ │ ├── values │ │ │ │ │ │ │ └── styles.xml │ │ │ │ │ │ └── values-night │ │ │ │ │ │ │ └── styles.xml │ │ │ │ │ ├── kotlin │ │ │ │ │ │ └── com │ │ │ │ │ │ │ └── weynard02 │ │ │ │ │ │ │ └── crud_local_database_app │ │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ ├── debug │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ └── profile │ │ │ │ │ └── AndroidManifest.xml │ │ │ └── build.gradle │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ └── gradle-wrapper.properties │ │ ├── .gitignore │ │ ├── settings.gradle │ │ └── build.gradle │ ├── lib │ │ ├── models │ │ │ ├── note.dart │ │ │ ├── note_database.dart │ │ │ └── note.g.dart │ │ ├── main.dart │ │ └── pages │ │ │ └── notes_page.dart │ ├── README.md │ ├── .gitignore │ ├── .metadata │ ├── test │ │ └── widget_test.dart │ ├── analysis_options.yaml │ └── pubspec.yaml └── README.md ├── 09. Firebase Auth ├── images │ ├── 06-add-auth.png │ ├── 03-add-flutter.png │ ├── 04-add-flutter.png │ ├── 05-add-flutter.png │ ├── 07-enable-email.png │ ├── 08-enable-email.png │ ├── 09-login-screen.png │ ├── 11-home-screen.png │ ├── 12-manage-user.png │ ├── 01-create-project.png │ ├── 02-create-project.png │ └── 10-register-screen.png └── README.md ├── 02. Creating App, Scaffold, AppBar, Colours, Fonts ├── images │ ├── VDM.png │ ├── app.png │ ├── AVD_name.png │ ├── appbar.png │ ├── colors.png │ ├── start-vd.png │ ├── vd-play.png │ ├── android_VD.png │ ├── app-vscode.png │ ├── choose_VD.png │ ├── create_VD.png │ ├── create_app.png │ ├── locate_SDK.png │ ├── new-window.png │ ├── start_page.png │ ├── text-style.png │ ├── tips_wrap.png │ ├── end_scaffold.png │ ├── fonts_folder.png │ ├── name-vscode.png │ ├── plugininstall.png │ ├── result-amber.png │ ├── result-font.png │ ├── start-vscode.png │ ├── emulator_vscode.png │ ├── launch-emulator.png │ ├── new_project_vscode.png │ ├── project-app-vscode.png │ ├── start_page_vscode.png │ ├── emulator_select_vscode.png │ └── new_flutter_project_created.png ├── README.md ├── coloursfont.md ├── scaffold.md └── create-app.md ├── 01. Introduction & Setup └── README.md ├── 03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns ├── images │ ├── 12-row.png │ ├── 05-icons.png │ ├── 13-column.png │ ├── 14-challenge.png │ ├── 06-icons-tips.png │ ├── 07-buttons-type.png │ ├── 08-buttons-style.png │ ├── 11-padding-widget.png │ ├── 01-widgets-composition.png │ ├── 04-images-asset-image.png │ ├── 02-images-network-image.png │ ├── 09-container-padding-margin.png │ ├── 03-images-asset-image-asset-dir.png │ └── 10-container-padding-margin-illustration.png └── README.md ├── README.md ├── 07. Relational Database └── README.md ├── 12. Access Resources └── README.md ├── 10. Awesome Notifications └── README.md ├── 08. Firebase └── README.md └── 04. Stateful and Starting the World Time App └── README.md /05. Form/images/form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/05. Form/images/form.png -------------------------------------------------------------------------------- /05. Form/images/controller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/05. Form/images/controller.png -------------------------------------------------------------------------------- /06. CRUD Local Database/images/hasil.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/06. CRUD Local Database/images/hasil.gif -------------------------------------------------------------------------------- /09. Firebase Auth/images/06-add-auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/09. Firebase Auth/images/06-add-auth.png -------------------------------------------------------------------------------- /09. Firebase Auth/images/03-add-flutter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/09. Firebase Auth/images/03-add-flutter.png -------------------------------------------------------------------------------- /09. Firebase Auth/images/04-add-flutter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/09. Firebase Auth/images/04-add-flutter.png -------------------------------------------------------------------------------- /09. Firebase Auth/images/05-add-flutter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/09. Firebase Auth/images/05-add-flutter.png -------------------------------------------------------------------------------- /09. Firebase Auth/images/07-enable-email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/09. Firebase Auth/images/07-enable-email.png -------------------------------------------------------------------------------- /09. Firebase Auth/images/08-enable-email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/09. Firebase Auth/images/08-enable-email.png -------------------------------------------------------------------------------- /09. Firebase Auth/images/09-login-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/09. Firebase Auth/images/09-login-screen.png -------------------------------------------------------------------------------- /09. Firebase Auth/images/11-home-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/09. Firebase Auth/images/11-home-screen.png -------------------------------------------------------------------------------- /09. Firebase Auth/images/12-manage-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/09. Firebase Auth/images/12-manage-user.png -------------------------------------------------------------------------------- /09. Firebase Auth/images/01-create-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/09. Firebase Auth/images/01-create-project.png -------------------------------------------------------------------------------- /09. Firebase Auth/images/02-create-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/09. Firebase Auth/images/02-create-project.png -------------------------------------------------------------------------------- /09. Firebase Auth/images/10-register-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/09. Firebase Auth/images/10-register-screen.png -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/VDM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/VDM.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/app.png -------------------------------------------------------------------------------- /01. Introduction & Setup/README.md: -------------------------------------------------------------------------------- 1 | # 01. Introduction & Setup 2 | 3 | [Main Page](/) | [Next](/02.%20Creating%20App,%20Scaffold,%20AppBar,%20Colours,%20Fonts/) 4 | 5 | ## Content Outline 6 | -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/AVD_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/AVD_name.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/appbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/appbar.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/colors.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/start-vd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/start-vd.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/vd-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/vd-play.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/android_VD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/android_VD.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/app-vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/app-vscode.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/choose_VD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/choose_VD.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/create_VD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/create_VD.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/create_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/create_app.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/locate_SDK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/locate_SDK.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/new-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/new-window.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/start_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/start_page.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/text-style.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/text-style.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/tips_wrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/tips_wrap.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/end_scaffold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/end_scaffold.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/fonts_folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/fonts_folder.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/name-vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/name-vscode.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/plugininstall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/plugininstall.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/result-amber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/result-amber.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/result-font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/result-font.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/start-vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/start-vscode.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/emulator_vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/emulator_vscode.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/launch-emulator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/launch-emulator.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/new_project_vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/new_project_vscode.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/project-app-vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/project-app-vscode.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/start_page_vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/start_page_vscode.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/emulator_select_vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/emulator_select_vscode.png -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/images/new_flutter_project_created.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/02. Creating App, Scaffold, AppBar, Colours, Fonts/images/new_flutter_project_created.png -------------------------------------------------------------------------------- /03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/12-row.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/12-row.png -------------------------------------------------------------------------------- /03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/05-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/05-icons.png -------------------------------------------------------------------------------- /03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/13-column.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/13-column.png -------------------------------------------------------------------------------- /03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/14-challenge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/14-challenge.png -------------------------------------------------------------------------------- /03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/06-icons-tips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/06-icons-tips.png -------------------------------------------------------------------------------- /03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/07-buttons-type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/07-buttons-type.png -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/06. CRUD Local Database/crud_local_database_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/06. CRUD Local Database/crud_local_database_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/08-buttons-style.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/08-buttons-style.png -------------------------------------------------------------------------------- /03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/11-padding-widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/11-padding-widget.png -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/06. CRUD Local Database/crud_local_database_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/06. CRUD Local Database/crud_local_database_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/06. CRUD Local Database/crud_local_database_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/01-widgets-composition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/01-widgets-composition.png -------------------------------------------------------------------------------- /03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/04-images-asset-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/04-images-asset-image.png -------------------------------------------------------------------------------- /03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/02-images-network-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/02-images-network-image.png -------------------------------------------------------------------------------- /03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/09-container-padding-margin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/09-container-padding-margin.png -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/app/src/main/kotlin/com/weynard02/crud_local_database_app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.weynard02.crud_local_database_app 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/03-images-asset-image-asset-dir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/03-images-asset-image-asset-dir.png -------------------------------------------------------------------------------- /03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/10-container-padding-margin-illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusbudi/mobile-programming/HEAD/03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/images/10-container-padding-margin-illustration.png -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip 6 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/lib/models/note.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:isar/isar.dart'; 4 | 5 | // this line is needed ot generate file 6 | // then run dart run build_runner build 7 | part 'note.g.dart'; 8 | 9 | @Collection() 10 | class Note { 11 | Id id = Isar.autoIncrement; 12 | late String text; 13 | 14 | late String imagePath; // stores path to the image file 15 | } -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/README.md: -------------------------------------------------------------------------------- 1 | # 02. Creating App, Scaffold, AppBar, Colors, Fonts 2 | 3 | [Previous](/01.%20Introduction%20&%20Setup/) | [Main Page](/) | [Next](/03.%20Widgets%20-%20Images,%20Buttons,%20Icons,%20Containers%20&%20Padding,%20Rows,%20Columns/) 4 | 5 | ## Content Outline 6 | 7 | ### [Creating a Flutter App](create-app.md) 8 | 9 | ### [Scaffold & AppBar Widgets](scaffold.md) 10 | 11 | ### [Colors & Font](coloursfont.md) 12 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/README.md: -------------------------------------------------------------------------------- 1 | # crud_local_database_app 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:crud_local_database_app/models/note_database.dart'; 2 | import 'package:crud_local_database_app/pages/notes_page.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | 7 | void main() async { 8 | WidgetsFlutterBinding.ensureInitialized(); 9 | await NoteDatabase.initialize(); 10 | 11 | runApp( 12 | ChangeNotifierProvider( 13 | create: (context) => NoteDatabase(), 14 | child: const MyApp(), 15 | ) 16 | ); 17 | } 18 | 19 | class MyApp extends StatelessWidget { 20 | const MyApp({super.key}); 21 | 22 | // This widget is the root of your application. 23 | @override 24 | Widget build(BuildContext context) { 25 | return const MaterialApp( 26 | debugShowCheckedModeBanner: false, 27 | home: NotesPage() 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Symbolication related 35 | app.*.symbols 36 | 37 | # Obfuscation related 38 | app.*.map.json 39 | 40 | # Android Studio will place build artifacts here 41 | /android/app/debug 42 | /android/app/profile 43 | /android/app/release 44 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | } 19 | 20 | plugins { 21 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 22 | id "com.android.application" version "8.3.2" apply false 23 | id "org.jetbrains.kotlin.android" version "1.7.10" apply false 24 | } 25 | 26 | include ":app" 27 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | afterEvaluate { project -> 11 | if (project.plugins.hasPlugin("com.android.application") || 12 | project.plugins.hasPlugin("com.android.library")) { 13 | project.android { 14 | compileSdkVersion 34 15 | buildToolsVersion "34.0.0" 16 | } 17 | } 18 | if (project.hasProperty("android")) { 19 | project.android { 20 | if (namespace == null) { 21 | namespace project.group 22 | } 23 | } 24 | } 25 | } 26 | project.buildDir = "${rootProject.buildDir}/${project.name}" 27 | project.evaluationDependsOn(':app') 28 | } 29 | 30 | tasks.register("clean", Delete) { 31 | delete rootProject.buildDir 32 | } 33 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d 17 | base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d 18 | - platform: android 19 | create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d 20 | base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:crud_local_database_app/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/lib/models/note_database.dart: -------------------------------------------------------------------------------- 1 | import 'package:crud_local_database_app/models/note.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:isar/isar.dart'; 4 | import 'package:path_provider/path_provider.dart'; 5 | import 'dart:io'; 6 | 7 | class NoteDatabase extends ChangeNotifier{ 8 | static late Isar isar; 9 | 10 | // INIT 11 | static Future initialize() async { 12 | if (Platform.isAndroid) { // Check if it's Android 13 | final dir = await getApplicationDocumentsDirectory(); 14 | isar = await Isar.open([NoteSchema], directory: dir.path); 15 | } else { 16 | // Handle other platforms or provide a default directory 17 | final dir = getTemporaryDirectory(); // Example for other platforms 18 | isar = await Isar.open([NoteSchema], directory: (await dir).path); 19 | } 20 | } 21 | 22 | // list 23 | final List currentNotes = []; 24 | 25 | // create 26 | void addNote(String text, String imagePath) async { 27 | final note = Note() 28 | ..text = text 29 | ..imagePath = imagePath; 30 | 31 | await isar.writeTxn(() async { 32 | await isar.notes.put(note); 33 | }); 34 | 35 | fetchNotes(); // refresh notes list 36 | } 37 | // read 38 | Future fetchNotes() async { 39 | List fetchedNotes = await isar.notes.where().findAll(); 40 | currentNotes.clear(); 41 | currentNotes.addAll(fetchedNotes); 42 | notifyListeners(); 43 | } 44 | // update 45 | Future updateNote(int id, String newText) async { 46 | final existingNote = await isar.notes.get(id); 47 | if (existingNote != null) { 48 | existingNote.text = newText; 49 | await isar.writeTxn(() => isar.notes.put(existingNote)); 50 | await fetchNotes(); 51 | } 52 | } 53 | // delete 54 | Future deleteNote(int id) async { 55 | await isar.writeTxn(() => isar.notes.delete(id)); 56 | await fetchNotes(); 57 | } 58 | } -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | namespace "com.weynard02.crud_local_database_app" 27 | compileSdk flutter.compileSdkVersion 28 | ndkVersion flutter.ndkVersion 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | } 38 | 39 | sourceSets { 40 | main.java.srcDirs += 'src/main/kotlin' 41 | } 42 | 43 | defaultConfig { 44 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 45 | applicationId "com.weynard02.crud_local_database_app" 46 | // You can update the following values to match your application needs. 47 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 48 | minSdkVersion flutter.minSdkVersion 49 | targetSdkVersion flutter.targetSdkVersion 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | } 53 | 54 | buildTypes { 55 | release { 56 | // TODO: Add your own signing config for the release build. 57 | // Signing with the debug keys for now, so `flutter run --release` works. 58 | signingConfig signingConfigs.debug 59 | } 60 | } 61 | } 62 | 63 | flutter { 64 | source '../..' 65 | } 66 | 67 | dependencies { 68 | } 69 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mobile Programming Tutorial 2 | 3 | ## Content Outline 4 | 5 | ### [01. Introduction & Setup](/01.%20Introduction%20&%20Setup/) 6 | 7 | ### [02. Creating App, Scaffold, AppBar, Colours, Fonts](/02.%20Creating%20App,%20Scaffold,%20AppBar,%20Colours,%20Fonts/) 8 | 9 | ### [03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns](/03.%20Widgets%20-%20Images,%20Buttons,%20Icons,%20Containers%20&%20Padding,%20Rows,%20Columns/) 10 | 11 | ### [04. Stateful and Starting the World Time App](/04.%20Stateful%20and%20Starting%20the%20World%20Time%20App/) 12 | 13 | ### [05. Form](/05.%20Form/) 14 | 15 | ### [06. CRUD Local Database](/06.%20CRUD%20Local%20Database/) 16 | 17 | ### [07. Relational Database](/07.%20Relational%20Database/) 18 | 19 | ### [08. Firebase](/08.%20Firebase/) 20 | 21 | ### [09. Firebase Auth](/09.%20Firebase%20Auth/) 22 | 23 | ### [10. Awesome Notifications](/10.%20Awesome%20Notifications/) 24 | 25 | ### [11. Connecting API - Machine Learning Service](/11.%20Connecting%20API%20-%20Machine%20Learning%20Service/) 26 | 27 | ### [12. Access Resources](/12.%20Access%20Resources/) 28 | 29 | ## How to Contribute - for Teaching Assistants 30 | 31 | 1. Open issue with the title of the topic you want to contribute to. 32 | 33 | - Click on the Issues tab and click on the New Issue button. 34 | 35 | ![Image](https://github.com/user-attachments/assets/58208693-df77-47f5-8534-8af72eab4ec0) 36 | 37 | - Add the title and assign yourself to the issue. 38 | 39 | ![Image](https://github.com/user-attachments/assets/6e5bfcee-6568-4aa6-bcef-d0ce605cba00) 40 | 41 | - Click on Create Issue. 42 | 43 | 2. Create a new branch with the name of the issue. 44 | 45 | - On the newly created issue, click on Create a branch. 46 | 47 | ![Image](https://github.com/user-attachments/assets/3134d5f3-9051-4f34-9897-59c7032321be) 48 | 49 | - Remove the first number on the branch name. 50 | 51 | ![Image](https://github.com/user-attachments/assets/0507e4f1-0170-46f4-b353-ac97440468d3) 52 | 53 | - Click on Create branch and copy the instructions. 54 | 55 | ![Image](https://github.com/user-attachments/assets/61367dea-7e02-43fc-b54f-3cc65b42a3f3) 56 | 57 | 3. Checkout to the new branch. 58 | 59 | - Open the terminal and paste the instructions. 60 | 61 | ![Image](https://github.com/user-attachments/assets/db566e82-cc2d-4c1c-95f3-1f2309959a7d) 62 | 63 | 4. Add your content. 64 | 65 | 5. Push the branch to the repository. 66 | 67 | - Open the terminal and run this command. 68 | 69 | ```bash 70 | git add . 71 | git commit -m 72 | git pull origin main --rebase 73 | git push 74 | ``` 75 | 76 | 6. Create a pull request to the main branch. 77 | 78 | - Open the branch and click on contribute, then open a pull request. 79 | 80 | ![Image](https://github.com/user-attachments/assets/a71df9ae-700e-4de4-8a66-a7bf1245749f) 81 | 82 | - Change the title and add lecturer as a reviewer as well as assign yourself. 83 | 84 | ![image](https://github.com/user-attachments/assets/e79ededd-3a1a-407a-8779-c33c17ad29b2) 85 | 86 | - Click on Create pull request. 87 | 88 | 7. Wait for the review and approval. 89 | 90 | 8. Merge the pull request after approval. 91 | 92 | - Go to the pull request and change to rebase and merge. 93 | 94 | ![image](https://github.com/user-attachments/assets/3c48771d-8cf8-4b3b-8c59-d4ef0d30dbd6) 95 | 96 | - Click on Rebase and merge. 97 | -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/coloursfont.md: -------------------------------------------------------------------------------- 1 | # Colors & Font 2 | 3 | [Back](README.md) 4 | 5 | ## Content Overview 6 | 7 | - [Colors & Font](#colors--font) 8 | - [Colors](#colors) 9 | - [Font](#font) 10 | - [Custom Font Family](#custom-font-family) 11 | 12 | ### Colors 13 | 14 | Previously, we used the default colors from the Material Design app. 15 | 16 | ```dart 17 | void main() { 18 | runApp(MaterialApp( 19 | home: Scaffold( 20 | appBar: AppBar( 21 | title: Text("My first app"), 22 | centerTitle: true, 23 | ), 24 | body: Center(child: Text("This is body")), 25 | floatingActionButton: FloatingActionButton( 26 | child: Text('click'), 27 | ), 28 | ))); 29 | } 30 | ``` 31 | 32 | Based on the previous code, let's change the AppBar background color using the `backgroundColor` property of the AppBar widget. Then, we can set the color using `Colors.`, which will show various available colors. 33 | 34 | ![colors](images/colors.png) 35 | 36 | As shown (for example, the Amber color), there are different shades ranging from 50, 100, 200, etc. This allows us to select the specific shade we want. The example code is as follows: 37 | 38 | ```dart 39 | AppBar( 40 | title: Text("My first app"), 41 | centerTitle: true, 42 | backgroundColor: Colors.amber[200] 43 | ), 44 | ``` 45 | 46 | ![img](images/result-amber.png) 47 | 48 | We can also change the color of the `FloatingActionButton`: 49 | 50 | ```dart 51 | FloatingActionButton( 52 | onPressed: () {}, 53 | child: Text('click'), 54 | backgroundColor: Colors.red[600], 55 | ) 56 | ``` 57 | 58 | For more details about colors, refer to the following link: 59 | [Flutter Colors API Reference](https://api.flutter.dev/flutter/material/Colors-class.html) 60 | 61 | If you want to use a custom hex color code, you can use the `Color()` widget: 62 | 63 | ```dart 64 | Color(0xFF42A5F5) 65 | ``` 66 | 67 | ### Font 68 | 69 | Besides applying colors to widgets like the AppBar and buttons, we can also modify the font style of text using the `Text` widget. This can be done using the `TextStyle` widget, which is applied through the `style` property of `Text`. 70 | 71 | ```dart 72 | Text( 73 | 'Hello World!', 74 | style: TextStyle( 75 | fontSize: 20.0, 76 | fontWeight: FontWeight.bold, 77 | letterSpacing: 2.0, 78 | color: Colors.grey[600], 79 | ), 80 | ) 81 | ``` 82 | 83 | Some properties we can adjust include font size (`fontSize`), font weight (`fontWeight`), letter spacing (`letterSpacing`), and color (`color`). 84 | 85 | ![textstyle](images/textstyle.png) 86 | 87 | #### Custom Font Family 88 | 89 | We can change the text font family to a custom one. First, download a font. A good source is [Google Fonts](https://fonts.google.com/). After downloading the font, move it into your Flutter project by creating a `/fonts` folder. 90 | 91 | ![fonts](images/fonts_folder.png) 92 | 93 | To let Flutter recognize the font, we need to configure the `pubspec.yaml` file by adding the following code (be mindful of whitespace formatting): 94 | 95 | ```yaml 96 | fonts: 97 | - family: Poppins 98 | fonts: 99 | - asset: fonts/Poppins-Regular.ttf 100 | ``` 101 | 102 | When saved, `flutter pub get` will automatically run to update dependencies, including the new font family. Now, we can use `Poppins` in the `fontFamily` property of `TextStyle`: 103 | 104 | ```dart 105 | Text( 106 | 'Hello World!', 107 | style: TextStyle( 108 | fontSize: 20.0, 109 | fontWeight: FontWeight.bold, 110 | letterSpacing: 2.0, 111 | color: Colors.grey[600], 112 | fontFamily: 'Poppins' 113 | ), 114 | ) 115 | ``` 116 | 117 | The end result should be as follows. 118 | 119 | ![Result font](images/result-font.png) 120 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: crud_local_database_app 2 | description: "A new Flutter project." 3 | # The following line prevents the package from being accidentally published to 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 5 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 6 | 7 | # The following defines the version and build number for your application. 8 | # A version number is three numbers separated by dots, like 1.2.43 9 | # followed by an optional build number separated by a +. 10 | # Both the version and the builder number may be overridden in flutter 11 | # build by specifying --build-name and --build-number, respectively. 12 | # In Android, build-name is used as versionName while build-number used as versionCode. 13 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 14 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 15 | # Read more about iOS versioning at 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 17 | # In Windows, build-name is used as the major, minor, and patch parts 18 | # of the product and file versions while build-number is used as the build suffix. 19 | version: 1.0.0+1 20 | 21 | environment: 22 | sdk: '>=3.3.4 <4.0.0' 23 | 24 | # Dependencies specify other packages that your package needs in order to work. 25 | # To automatically upgrade your package dependencies to the latest versions 26 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 27 | # dependencies can be manually updated by changing the version numbers below to 28 | # the latest version available on pub.dev. To see which dependencies have newer 29 | # versions available, run `flutter pub outdated`. 30 | dependencies: 31 | flutter: 32 | sdk: flutter 33 | 34 | 35 | # The following adds the Cupertino Icons font to your application. 36 | # Use with the CupertinoIcons class for iOS style icons. 37 | cupertino_icons: ^1.0.6 38 | isar: ^3.1.0+1 39 | isar_flutter_libs: ^3.1.0+1 40 | path_provider: ^2.1.4 41 | provider: ^6.1.4 42 | image_picker: ^1.0.4 43 | 44 | dev_dependencies: 45 | flutter_test: 46 | sdk: flutter 47 | 48 | # The "flutter_lints" package below contains a set of recommended lints to 49 | # encourage good coding practices. The lint set provided by the package is 50 | # activated in the `analysis_options.yaml` file located at the root of your 51 | # package. See that file for information about deactivating specific lint 52 | # rules and activating additional ones. 53 | flutter_lints: ^3.0.0 54 | isar_generator: ^3.1.0+1 55 | build_runner: ^2.4.9 56 | 57 | # For information on the generic Dart part of this file, see the 58 | # following page: https://dart.dev/tools/pub/pubspec 59 | 60 | # The following section is specific to Flutter packages. 61 | flutter: 62 | 63 | # The following line ensures that the Material Icons font is 64 | # included with your application, so that you can use the icons in 65 | # the material Icons class. 66 | uses-material-design: true 67 | 68 | # To add assets to your application, add an assets section, like this: 69 | # assets: 70 | # - images/a_dot_burr.jpeg 71 | # - images/a_dot_ham.jpeg 72 | 73 | # An image asset can refer to one or more resolution-specific "variants", see 74 | # https://flutter.dev/assets-and-images/#resolution-aware 75 | 76 | # For details regarding adding assets from package dependencies, see 77 | # https://flutter.dev/assets-and-images/#from-packages 78 | 79 | # To add custom fonts to your application, add a fonts section here, 80 | # in this "flutter" section. Each entry in this list should have a 81 | # "family" key with the font family name, and a "fonts" key with a 82 | # list giving the asset and other descriptors for the font. For 83 | # example: 84 | # fonts: 85 | # - family: Schyler 86 | # fonts: 87 | # - asset: fonts/Schyler-Regular.ttf 88 | # - asset: fonts/Schyler-Italic.ttf 89 | # style: italic 90 | # - family: Trajan Pro 91 | # fonts: 92 | # - asset: fonts/TrajanPro.ttf 93 | # - asset: fonts/TrajanPro_Bold.ttf 94 | # weight: 700 95 | # 96 | # For details regarding fonts from package dependencies, 97 | # see https://flutter.dev/custom-fonts/#from-packages 98 | -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/scaffold.md: -------------------------------------------------------------------------------- 1 | # Scaffold & AppBar Widgets 2 | 3 | [Back](README.md) 4 | 5 | ## Content Overview 6 | 7 | - [Scaffold & AppBar Widgets](#scaffold--appbar-widgets) 8 | - [Scaffold](#scaffold) 9 | - [AppBar](#appbar) 10 | - [Body](#body) 11 | - [Floating Action Button](#floating-action-button) 12 | - [Tips](#tips) 13 | 14 | With the Flutter application ready, we can start to make our application by using widgets. Prepare the program as follows to continue. 15 | 16 | ```dart 17 | import 'package:flutter/material.dart'; 18 | 19 | void main() { 20 | runApp(MaterialApp( 21 | home: Text("Hey MOBILE"), 22 | )); 23 | } 24 | ``` 25 | 26 | ## Scaffold 27 | 28 | Scaffold class widget is a basic Material Design visual layout structure. It allows to setup the AppBar, FloatingActionButton, and more. We can start by using `Scaffold()` on our `MaterialApp()` 29 | 30 | ```dart 31 | import 'package:flutter/material.dart'; 32 | 33 | void main() { 34 | runApp(MaterialApp( 35 | home: Scaffold(), 36 | )); 37 | } 38 | ``` 39 | 40 | Reference: https://api.flutter.dev/flutter/material/Scaffold-class.html 41 | 42 | We can move on to setup the appbar and the body 43 | 44 | ### AppBar 45 | 46 | We use `appBar` property from Scaffold to setup our AppBar. The value of this `appBar` is an `AppBar` Widget. `AppBar` Widget has some properties like `title`, `leading`, `actions`, and more. 47 | 48 | ![Appbar](images/appbar.png) 49 | 50 | Let's try add AppBar and give title on the AppBar by adding title with a `Text` widget. 51 | 52 | ```dart 53 | import 'package:flutter/material.dart'; 54 | 55 | void main() { 56 | runApp(MaterialApp( 57 | home: Scaffold( 58 | appBar: AppBar( 59 | title: Text('my first app') 60 | ) 61 | ), 62 | )); 63 | } 64 | ``` 65 | 66 | Run or restart the program and see how it results. There are other properties for AppBar, like `centerTitle: true` for centering the text. 67 | 68 | Reference: https://api.flutter.dev/flutter/material/AppBar-class.html 69 | 70 | ### Body 71 | 72 | There is also `body` property from Scaffold widget for content app. We can put `Text` widget on the body for its content. 73 | 74 | ```dart 75 | import 'package:flutter/material.dart'; 76 | 77 | void main() { 78 | runApp(MaterialApp( 79 | home: Scaffold( 80 | appBar: AppBar( 81 | title: Text('my first app') 82 | ), 83 | body: Text("This is body") 84 | ), 85 | )); 86 | } 87 | ``` 88 | 89 | What if we have multiple widgets that we want to place in the `body` or configure the content? We can wrap them using widgets such as: 90 | 91 | 1. Container 92 | 93 | A versatile widget that allows you to apply padding, margins, alignment, decoration (such as background color and border), and constraints to its child. 94 | 95 | ```dart 96 | Container( 97 | padding: EdgeInsets.all(10), 98 | color: Colors.blue, 99 | child: Text("This is body"), 100 | ) 101 | ``` 102 | 103 | 2. Center 104 | 105 | Centers its child widget both vertically and horizontally within the available space. 106 | 107 | ```dart 108 | Center( 109 | child: Text("Hello, Flutter!"), 110 | ) 111 | ``` 112 | 113 | 3. Row 114 | 115 | A widget that arranges its children horizontally (from left to right). 116 | 117 | ```dart 118 | Row( 119 | mainAxisAlignment: MainAxisAlignment.center, 120 | children: [ 121 | Text("Hello"), 122 | SizedBox(width: 10), 123 | Text("Flutter!"), 124 | ], 125 | ) 126 | ``` 127 | 128 | 4. Column 129 | 130 | Similar to Row, but arranges its children vertically (from top to bottom). 131 | 132 | ```dart 133 | Column( 134 | mainAxisAlignment: MainAxisAlignment.center, 135 | children: [ 136 | Text("Hello"), 137 | SizedBox(height: 10), 138 | Text("Flutter!"), 139 | ], 140 | ) 141 | ``` 142 | 143 | 5. Stack 144 | 145 | Overlays multiple widgets on top of each other. 146 | 147 | 6. ListView 148 | 149 | A scrollable list of widgets. 150 | 151 | 7. Expanded/Flexible 152 | 153 | Helps distribute space when using Row or Column. 154 | 155 | We try center the `Text` by wrapping it up with `Center`, the `Text` is placed inside the child. 156 | 157 | ```dart 158 | void main() { 159 | runApp(MaterialApp( 160 | home: Scaffold( 161 | appBar: AppBar( 162 | title: Text("My first app"), 163 | centerTitle: true, 164 | ), 165 | body: Center(child: Text("This is body")), 166 | ))); 167 | } 168 | ``` 169 | 170 | The end result should be as follows 171 | 172 | ![EndScaffold](images/end_scaffold.png) 173 | 174 | ### Floating Action Button 175 | 176 | A Material Design floating action button. A floating action button is a circular icon button that hovers over content to promote a primary action in the application. Floating action buttons are most commonly used in the Scaffold.floatingActionButton field. 177 | 178 | ```dart 179 | void main() { 180 | runApp(MaterialApp( 181 | home: Scaffold( 182 | appBar: AppBar( 183 | title: Text("My first app"), 184 | centerTitle: true, 185 | ), 186 | body: Center(child: Text("This is body")), 187 | floatingActionButton: FloatingActionButton( 188 | onPressed: () { }, 189 | child: Text('click'), 190 | ), 191 | ))); 192 | } 193 | ``` 194 | 195 | ## Tips 196 | 197 | You can click on the lamp icon to wrap content automatically 198 | ![tipswrap](images/tips_wrap.png) 199 | -------------------------------------------------------------------------------- /07. Relational Database/README.md: -------------------------------------------------------------------------------- 1 | # 07. Relational Database 2 | 3 | [Previous](/06.%20CRUD%20Local%20Database/) | [Main Page](/) | [Next](/07.%20Awesome%20Notifications/) 4 | 5 | ## Content Outline 6 | 7 | - [One-to-one](#one-to-one-relationship) 8 | - [One-to-many](#one-to-many-relationship) 9 | - [Many-to-many](#many-to-many-relationship) 10 | 11 | This module covers relationship between two objects in database Isar. 12 | 13 | ## One-to-one relationship 14 | 15 | In a one-to-one relationship, one object in a collection is related to only one object in another collection and vice versa. In example, a `User` has one `Profile`, and each `Profile` belongs to one `User`. 16 | 17 | To model this, we use `IsarLink` type to link between two classes. 18 | 19 | ```dart 20 | @Collection() 21 | class User { 22 | Id id = Isar.autoIncrement; 23 | 24 | late String name; 25 | 26 | // One-to-one link to Profile 27 | final profile = IsarLink(); 28 | } 29 | 30 | @Collection() 31 | class Profile { 32 | Id id = Isar.autoIncrement; 33 | 34 | late String bio; 35 | late String avatarUrl; 36 | } 37 | ``` 38 | 39 | `User` has a profile property of type `IsarLink()`. 40 | 41 | Here is the example how to use this relationship 42 | 43 | ```dart 44 | // Create object relation 45 | final user = User()..name = 'Alice'; 46 | final profile = Profile() 47 | ..bio = 'Hello!' 48 | ..avatarUrl = 'https://image.url'; 49 | 50 | // Link them together 51 | user.profile.value = profile; 52 | 53 | await isar.writeTxn(() async { 54 | await isar.profiles.put(profile); // save the profile first 55 | await isar.users.put(user); // then save the user 56 | }); 57 | ``` 58 | 59 | ```dart 60 | // How to access the linked profile 61 | final loadedUser = await isar.users.get(user.id); 62 | 63 | // Load the linked profile 64 | await loadedUser!.profile.load(); 65 | 66 | print(loadedUser.profile.value?.bio); // Output: Hello! 67 | 68 | ``` 69 | 70 | ```dart 71 | // How to remove the link 72 | user.profile.value = null; 73 | 74 | await isar.writeTxn(() async { 75 | await isar.users.put(user); // Update the link 76 | }); 77 | ``` 78 | 79 | ## One-to-many relationship 80 | 81 | In a one-to-many relationship, one object in a collection is related to many one object in another collection. In example, a `User` can have many `Post`s, but each `Post` belongs to one `User`. 82 | 83 | In Isar, One-to-Many is modeled using a reverse link from the “many” side with `IsarLink`. 84 | 85 | ```dart 86 | @Collection() 87 | class User { 88 | Id id = Isar.autoIncrement; 89 | 90 | late String name; 91 | // No need to explicitly define the one-to-many here 92 | } 93 | 94 | @Collection() 95 | class Post { 96 | Id id = Isar.autoIncrement; 97 | 98 | late String title; 99 | late String content; 100 | 101 | final user = IsarLink(); // Many-to-One link 102 | } 103 | 104 | ``` 105 | 106 | Here is the example how to use this relationship 107 | 108 | ```dart 109 | // Create object relation 110 | final user = User()..name = 'Alice'; 111 | 112 | final post1 = Post() 113 | ..title = 'My First Post' 114 | ..content = 'Hello world!' 115 | ..user.value = user; 116 | 117 | final post2 = Post() 118 | ..title = 'Another Post' 119 | ..content = 'More content here' 120 | ..user.value = user; 121 | 122 | await isar.writeTxn(() async { 123 | await isar.users.put(user); // Save the user first 124 | await isar.posts.putAll([post1, post2]); // Then the posts 125 | }); 126 | 127 | ``` 128 | 129 | ```dart 130 | // Get all posts for a user 131 | 132 | final userId = user.id; 133 | 134 | final posts = await isar.posts 135 | .filter() 136 | .user((q) => q.idEqualTo(userId)) 137 | .findAll(); 138 | 139 | for (final post in posts) { 140 | print(post.title); 141 | } 142 | ``` 143 | 144 | ### Many-to-many relationship 145 | 146 | In a many-to-many relationship, many object in a collection is related to many one object in another collection. In example, a `Student` can enroll to many `Course`s, and a `Course` can have many `Student`s. 147 | 148 | In Isar, you use `IsarLinks` on both sides of the relationship. 149 | 150 | ```dart 151 | @Collection() 152 | class Student { 153 | Id id = Isar.autoIncrement; 154 | 155 | late String name; 156 | 157 | final courses = IsarLinks(); // Many-to-Many 158 | } 159 | 160 | @Collection() 161 | class Course { 162 | Id id = Isar.autoIncrement; 163 | 164 | late String title; 165 | 166 | final students = IsarLinks(); // Many-to-Many 167 | } 168 | 169 | ``` 170 | 171 | Here is the example how to use this relationship 172 | 173 | ```dart 174 | final math = Course()..title = 'Math'; 175 | final science = Course()..title = 'Science'; 176 | 177 | final alice = Student()..name = 'Alice'; 178 | final bob = Student()..name = 'Bob'; 179 | 180 | // Link students to courses 181 | alice.courses.addAll([math, science]); 182 | bob.courses.add(math); 183 | 184 | // Link courses to students (optional but recommended for bi-directional access) 185 | math.students.addAll([alice, bob]); 186 | science.students.add(alice); 187 | 188 | await isar.writeTxn(() async { 189 | await isar.courses.putAll([math, science]); 190 | await isar.students.putAll([alice, bob]); 191 | 192 | // Save the links 193 | await alice.courses.save(); 194 | await bob.courses.save(); 195 | await math.students.save(); 196 | await science.students.save(); 197 | }); 198 | 199 | ``` 200 | -------------------------------------------------------------------------------- /02. Creating App, Scaffold, AppBar, Colours, Fonts/create-app.md: -------------------------------------------------------------------------------- 1 | # Creating a Flutter App 2 | 3 | [Back](README.md) 4 | 5 | ## Content Outline 6 | 7 | - [Creating a Flutter App](#creating-a-flutter-app) 8 | - [Android Studio](#android-studio) 9 | - [1. Creating a Virtual Device](#1-creating-a-virtual-device) 10 | - [2. Installing the Flutter Plugin](#2-installing-the-flutter-plugin) 11 | - [3. Starting a Flutter Application](#3-starting-a-flutter-application) 12 | - [4. Run the Project](#4-run-the-project) 13 | - [Visual Studio Code](#visual-studio-code) 14 | - [1. Starting a Flutter Application](#1-starting-a-flutter-application) 15 | - [First program](#first-program) 16 | - [Note](#note) 17 | 18 | After installing Flutter in [01. Introduction & Setup](s/01.%20Introduction%20&%20Setup/), we can start creating our first Flutter application. There are two IDEs (Integrated Development Environments) that can be used to build a Flutter app: Android Studio and Visual Studio Code. 19 | 20 | ## Android Studio 21 | 22 | Android Studio is a popular IDE for mobile app development. After installing [Android Studio](https://developer.android.com/studio?gad_source=1&gclid=Cj0KCQiA2oW-BhC2ARIsADSIAWoAPGSFYZetK8lZ7chW-gRH1ND2PYcij3Ty7qLUqhh3ljrh3Oc-4DoaAo2qEALw_wcB&gclsrc=aw.ds), its interface will look like this: 23 | 24 | ![img](images/new-window.png) 25 | 26 | ### 1. Creating a Virtual Device 27 | 28 | One option to preview the application interface is by using a Virtual Device (Emulator). The first step is to find the **Virtual Device Manager** menu as shown below: 29 | 30 | ![VDM](images/VDM.png) 31 | 32 | Then, we will add a new Virtual Device by selecting the **Create Virtual Device** option. 33 | 34 | ![Create Virtual Device](images/create_VD.png) 35 | 36 | Next, choose the appropriate phone hardware based on your needs. 37 | 38 | ![Choose](images/choose_VD.png) 39 | 40 | Then, select the Android OS version. 41 | 42 | ![Android version](images/android_VD.png) 43 | 44 | Finally, name your Android Virtual Device and press **Finish**. 45 | 46 | ![AVD name](images/AVD_name.png) 47 | 48 | Once the Virtual Device setup is complete, it will be ready for use as an emulator. 49 | 50 | ### 2. Installing the Flutter Plugin 51 | 52 | The next step is to install the Flutter Plugin in Android Studio to enable Flutter development. Navigate to the **Plugins** page → Marketplace → Search for `Flutter`. 53 | 54 | ![Flutter Plugin](images/plugininstall.png) 55 | 56 | Install the plugin so that the `New Flutter Project` option appears. 57 | 58 | ![New Flutter Project](images/new_flutter_project_created.png) 59 | 60 | ### 3. Starting a Flutter Application 61 | 62 | You can create your first Flutter application by selecting **New Flutter Project**. 63 | 64 | ![New Flutter Project](images/new_flutter_project_created.png) 65 | 66 | Then, locate the previously installed Flutter SDK. 67 | 68 | ![Flutter SDK](images/locate_SDK.png) 69 | 70 | Next, configure the `Project Name`, `Project Location`, `Description`, and `Organization Name`. 71 | 72 | ![Flutter New App](images/create_app.png) 73 | 74 | Once everything is set up, the editor interface will appear as follows: 75 | 76 | ![Start page](images/start_page.png) 77 | 78 | #### 4. Run the Project 79 | 80 | We need to start up the emulator by pressing play button on `Device Manager` 81 | 82 | ![StartVD](images/start-vd.png) 83 | 84 | We can try to run project by pressing the play green button on the top right or next to `main()` function. 85 | 86 | ![StartApp](images/vd-play.png) 87 | 88 | The application will be shown as follows 89 | 90 | ![app](images/app.png) 91 | 92 | ## Visual Studio Code 93 | 94 | ### 1. Starting a Flutter Application 95 | 96 | In Visual Studio Code, make sure you have installed the `Flutter` extension. 97 | 98 | ![Vscode](images/start-vscode.png) 99 | 100 | To create a new Flutter project, press `Ctrl + Shift + P` and search for `Flutter: New Project`. 101 | 102 | ![New Project](images/new_project_vscode.png) 103 | 104 | Then, select `Application` and choose the project storage location. 105 | 106 | ![Application](images/project-app-vscode.png) 107 | 108 | Finally, enter the name of the project you want to create. 109 | 110 | ![Name vscode](images/name-vscode.png) 111 | 112 | The project structure will be created and displayed as follows: 113 | 114 | ![Start page](images/start_page_vscode.png) 115 | 116 | You can see a code snippet that is ready to be tested. However, before running the application, you need an Android phone or an emulator. 117 | 118 | One way to set up an emulator is by creating a [Virtual Device](#1-creating-a-virtual-device) in Android Studio. Then, in Visual Studio Code, press `Ctrl + Shift + P` and search for `Flutter: Launch Emulator`. 119 | 120 | ![launch emulator](images/launch-emulator.png) 121 | 122 | Next, select the emulator you want to use. 123 | 124 | ![Emulator](images/emulator_select_vscode.png) 125 | 126 | Once launched, the Android Virtual Device emulator will appear. 127 | 128 | ![Emulator](images/emulator_vscode.png) 129 | 130 | To run the application, you can type `flutter run` in the terminal or press `Run` on the `main()` function. This will install the application on the Android device and display it on the screen. 131 | 132 | ![Alt text](images/app-vscode.png) 133 | 134 | ## First Program 135 | 136 | Create a simple program by using `void main()` function as follows 137 | 138 | ```dart 139 | import 'package:flutter/material.dart'; // Material Design package 140 | 141 | void main() { 142 | runApp(MaterialApp( 143 | home: Text("Hey MOBILE"), 144 | )); 145 | } 146 | ``` 147 | 148 | We use `runApp` to run application with `MaterialApp` for using its Material Design. 149 | To give a text on our `MaterialApp`, we need to use `Text()` widget. Run and see the result! 150 | 151 | ## Note 152 | 153 | If there is an error while running the application, try change the gradle version on `android\gradle\wrapper\gradle-wrapper.properties` based on the Java version used on Flutter by typing `flutter doctor --verbose` on terminal. 154 | 155 | ``` 156 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip 157 | ``` 158 | 159 | Check the gradle version on 160 | https://docs.gradle.org/current/userguide/compatibility.html#java 161 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/lib/pages/notes_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:crud_local_database_app/models/note.dart'; 2 | import 'package:crud_local_database_app/models/note_database.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:path_provider/path_provider.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'dart:io'; 7 | import 'package:image_picker/image_picker.dart'; 8 | 9 | class NotesPage extends StatefulWidget { 10 | const NotesPage({super.key}); 11 | 12 | @override 13 | State createState() => _NotesPageState(); 14 | } 15 | 16 | class _NotesPageState extends State { 17 | // text controller to access what the user typed 18 | final textController = TextEditingController(); 19 | String? _pickedImagePath; 20 | 21 | final ImagePicker _picker = ImagePicker(); 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | readNotes(); 27 | } 28 | // Pick the image from camera and save it on system 29 | Future _pickImageAndSave() async { 30 | // Pick image using the image picker 31 | final XFile? pickedImage = await _picker.pickImage(source: ImageSource.camera); 32 | 33 | if (pickedImage != null) { 34 | // Get the directory to save the image 35 | final directory = await getApplicationDocumentsDirectory(); 36 | final fileName = 'image_${DateTime.now().millisecondsSinceEpoch}.jpg'; 37 | final imagePath = '${directory.path}/$fileName'; 38 | 39 | // Save the image to the app's document directory 40 | final File imageFile = File(imagePath); 41 | await imageFile.writeAsBytes(await pickedImage.readAsBytes()); 42 | return imagePath; 43 | } 44 | return null; 45 | } 46 | 47 | // create a note 48 | void createNote() { 49 | showDialog( 50 | context: context, 51 | builder: (context) => StatefulBuilder( 52 | builder: (context, setState) => AlertDialog( 53 | title: const Text("Create Note"), 54 | content: SingleChildScrollView( 55 | child: Column( 56 | mainAxisSize: MainAxisSize.min, 57 | children: [ 58 | TextField( 59 | controller: textController, 60 | decoration: const InputDecoration(labelText: "Note text"), 61 | ), 62 | const SizedBox(height: 10), 63 | _pickedImagePath != null 64 | ? Image.file(File(_pickedImagePath!), height: 150) 65 | : const Text("No image selected"), 66 | TextButton.icon( 67 | onPressed: () async { 68 | // Pick and save image 69 | final imagePath = await _pickImageAndSave(); 70 | if (imagePath != null) { 71 | setState(() { 72 | _pickedImagePath = imagePath; 73 | // print("WEYNARD $_pickedImagePath"); 74 | }); 75 | } 76 | }, 77 | icon: const Icon(Icons.camera_alt), 78 | label: const Text("Take Photo"), 79 | ), 80 | ], 81 | ), 82 | ), 83 | actions: [ 84 | TextButton( 85 | onPressed: () { 86 | Navigator.pop(context); 87 | textController.clear(); 88 | }, 89 | child: const Text("Cancel"), 90 | ), 91 | ElevatedButton( 92 | onPressed: () async { 93 | if (_pickedImagePath != null && textController.text.isNotEmpty) { 94 | // Save the note with the image path 95 | context.read().addNote( 96 | textController.text, 97 | _pickedImagePath!, 98 | ); 99 | textController.clear(); 100 | _pickedImagePath = null; 101 | Navigator.pop(context); 102 | } 103 | }, 104 | child: const Text("Create"), 105 | ), 106 | ], 107 | ), 108 | ), 109 | ); 110 | } 111 | 112 | // read notes 113 | void readNotes() { 114 | context.read().fetchNotes(); // Use read instead of watch 115 | } 116 | 117 | // update a note 118 | void updateNote(Note note) { 119 | textController.text = note.text; 120 | showDialog( 121 | context: context, 122 | builder: (context) => AlertDialog( 123 | title: Text("Update Note"), 124 | content: TextField(controller: textController), 125 | actions: [ 126 | MaterialButton( 127 | onPressed: () { 128 | context 129 | .read() 130 | .updateNote(note.id, textController.text); 131 | // clear controller 132 | textController.clear(); 133 | 134 | Navigator.pop(context); 135 | }, 136 | child: const Text("Update")) 137 | ], 138 | )); 139 | } 140 | 141 | // delete a note 142 | void deleteNote(int id) { 143 | context.read().deleteNote(id); 144 | } 145 | 146 | @override 147 | Widget build(BuildContext context) { 148 | // note database 149 | final noteDatabase = context.watch(); 150 | 151 | // current notes 152 | List currentNotes = noteDatabase.currentNotes; 153 | 154 | return Scaffold( 155 | appBar: AppBar(title: const Text('Notes')), 156 | floatingActionButton: FloatingActionButton( 157 | onPressed: createNote, 158 | child: const Icon(Icons.add), 159 | ), 160 | body: ListView.builder( 161 | itemCount: currentNotes.length, 162 | itemBuilder: (context, index) { 163 | // get individual note 164 | final note = currentNotes[index]; 165 | 166 | // list tile UI 167 | return ListTile( 168 | leading: note.imagePath.isNotEmpty 169 | ? Image.file(File(note.imagePath), width: 50, height: 50, fit: BoxFit.cover) 170 | : const Icon(Icons.image_not_supported), 171 | title: Text(note.text), 172 | trailing: Row( 173 | mainAxisSize: MainAxisSize.min, 174 | children: [ 175 | IconButton( 176 | onPressed: () => updateNote(note), 177 | icon: const Icon(Icons.edit), 178 | ), 179 | IconButton( 180 | onPressed: () => deleteNote(note.id), 181 | icon: const Icon(Icons.delete), 182 | ), 183 | ], 184 | ), 185 | ); 186 | }, 187 | )); 188 | } 189 | } -------------------------------------------------------------------------------- /12. Access Resources/README.md: -------------------------------------------------------------------------------- 1 | # 10 Access Resources 2 | 3 | [Previous](/09.%20Connecting%20API%20-%20Machine%20Learning%20Service/) | [Main Page](/) | 4 | 5 | ## Content Outline 6 | - [Camera](#camera) 7 | - [GPS](#gps) 8 | 9 | in this module we will try how to access and use some of the resources in flutter 10 | 11 | ## Camera 12 | 13 | We can access camera using the [`camera`](https://pub.dev/packages/camera) plugin. It show a live camera preview and allows users to take a picture. 14 | 15 | first you must set your depedencies `pubspec.yaml`: 16 | ``` 17 | dependencies: 18 | flutter: 19 | sdk: flutter 20 | camera: ^0.10.5+7 21 | ``` 22 | 23 | after that you have to add this syntax to your `app/src/main/AndroidManifest.xml`: 24 | 25 | ``` 26 | 27 | 28 | ``` 29 | 30 | after that you can make your own kind of app or implement this simple code in `main.dart`: 31 | 32 | ``` 33 | import 'package:flutter/material.dart'; 34 | import 'package:camera/camera.dart'; 35 | 36 | void main() async { 37 | WidgetsFlutterBinding.ensureInitialized(); 38 | final cameras = await availableCameras(); 39 | final firstCamera = cameras.first; 40 | 41 | runApp(MyApp(camera: firstCamera)); 42 | } 43 | 44 | class MyApp extends StatelessWidget { 45 | final CameraDescription camera; 46 | 47 | const MyApp({super.key, required this.camera}); 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | return MaterialApp( 52 | home: CameraPreviewScreen(camera: camera), 53 | ); 54 | } 55 | } 56 | 57 | class CameraPreviewScreen extends StatefulWidget { 58 | final CameraDescription camera; 59 | 60 | const CameraPreviewScreen({super.key, required this.camera}); 61 | 62 | @override 63 | State createState() => _CameraPreviewScreenState(); 64 | } 65 | 66 | class _CameraPreviewScreenState extends State { 67 | late CameraController _controller; 68 | late Future _initializeControllerFuture; 69 | 70 | @override 71 | void initState() { 72 | super.initState(); 73 | _controller = CameraController( 74 | widget.camera, 75 | ResolutionPreset.high, 76 | ); 77 | 78 | _initializeControllerFuture = _controller.initialize(); 79 | } 80 | 81 | @override 82 | void dispose() { 83 | _controller.dispose(); 84 | super.dispose(); 85 | } 86 | 87 | Future takePicture() async { 88 | try { 89 | await _initializeControllerFuture; 90 | final image = await _controller.takePicture(); 91 | ScaffoldMessenger.of(context).showSnackBar( 92 | SnackBar(content: Text('Picture saved to ${image.path}')), 93 | ); 94 | } catch (e) { 95 | print('Error taking picture: $e'); 96 | } 97 | } 98 | 99 | @override 100 | Widget build(BuildContext context) { 101 | return Scaffold( 102 | appBar: AppBar(title: const Text('Camera Access')), 103 | body: FutureBuilder( 104 | future: _initializeControllerFuture, 105 | builder: (context, snapshot) { 106 | if (snapshot.connectionState == ConnectionState.done) { 107 | return CameraPreview(_controller); 108 | } else { 109 | return const Center(child: CircularProgressIndicator()); 110 | } 111 | }, 112 | ), 113 | floatingActionButton: FloatingActionButton( 114 | onPressed: takePicture, 115 | child: const Icon(Icons.camera), 116 | ), 117 | ); 118 | } 119 | } 120 | ``` 121 | 122 | ![image-1](https://github.com/user-attachments/assets/c9ff3895-0e6d-491b-ad2a-353b5ec2f3c6) 123 | 124 | 125 | ## GPS 126 | We can access location using [`flutter_map`](https://pub.dev/packages/flutter_map) and [`location`](https://pub.dev/packages/location) plugins 127 | 128 | first you must to setup your depedencies `pubspec.yaml`: 129 | 130 | ``` 131 | dependencies: 132 | flutter: 133 | sdk: flutter 134 | flutter_map: ^6.2.1 135 | latlong2: ^0.9.0 136 | location: ^5.0.3 137 | ``` 138 | 139 | after that add your setup at `app/src/main/AndroidManifest.xml`: 140 | 141 | ``` 142 | 143 | 144 | _MapWithLocationState(); 176 | } 177 | 178 | class _MapWithLocationState extends State { 179 | final Location _location = Location(); 180 | LocationData? _currentLocation; 181 | final MapController _mapController = MapController(); 182 | 183 | @override 184 | void initState() { 185 | super.initState(); 186 | _initializeLocation(); 187 | } 188 | 189 | Future _initializeLocation() async { 190 | final hasPermission = await _checkPermission(); 191 | if (!hasPermission) return; 192 | 193 | final loc = await _location.getLocation(); 194 | setState(() { 195 | _currentLocation = loc; 196 | }); 197 | 198 | // Listen to location changes 199 | _location.onLocationChanged.listen((newLoc) { 200 | setState(() => _currentLocation = newLoc); 201 | _mapController.move( 202 | LatLng(newLoc.latitude!, newLoc.longitude!), _mapController.zoom); 203 | }); 204 | } 205 | 206 | Future _checkPermission() async { 207 | bool serviceEnabled = await _location.serviceEnabled(); 208 | if (!serviceEnabled) { 209 | serviceEnabled = await _location.requestService(); 210 | if (!serviceEnabled) return false; 211 | } 212 | 213 | PermissionStatus permissionGranted = await _location.hasPermission(); 214 | if (permissionGranted == PermissionStatus.denied) { 215 | permissionGranted = await _location.requestPermission(); 216 | if (permissionGranted != PermissionStatus.granted) return false; 217 | } 218 | 219 | return true; 220 | } 221 | 222 | @override 223 | Widget build(BuildContext context) { 224 | return Scaffold( 225 | appBar: AppBar( 226 | title: Text("Track My Location"), 227 | ), 228 | body: _currentLocation == null 229 | ? Center(child: CircularProgressIndicator()) 230 | : FlutterMap( 231 | mapController: _mapController, 232 | options: MapOptions( 233 | center: LatLng( 234 | _currentLocation!.latitude!, _currentLocation!.longitude!), 235 | zoom: 16, 236 | ), 237 | children: [ 238 | TileLayer( 239 | urlTemplate: 240 | "https://tile.openstreetmap.org/{z}/{x}/{y}.png", 241 | ), 242 | MarkerLayer( 243 | markers: [ 244 | Marker( 245 | width: 60, 246 | height: 60, 247 | point: LatLng( 248 | _currentLocation!.latitude!, _currentLocation!.longitude!), 249 | child: Icon(Icons.my_location, 250 | color: Colors.blue, size: 40), 251 | ) 252 | ], 253 | ), 254 | ], 255 | ), 256 | ); 257 | } 258 | } 259 | ``` 260 | 261 | ![image](https://github.com/user-attachments/assets/33b9c602-8d5e-4633-b2b9-f1db5c82222e) 262 | -------------------------------------------------------------------------------- /03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns/README.md: -------------------------------------------------------------------------------- 1 | # 03. Widgets - Images, Buttons, Icons, Containers & Padding, Rows, Columns 2 | 3 | [Previous](/02.%20Creating%20App,%20Scaffold,%20AppBar,%20Colours,%20Fonts/) | [Main Page](/) | [Next](/04.%20Stateful%20and%20Starting%20the%20World%20Time%20App/) 4 | 5 | ## Content Outline 6 | 7 | - [Widgets](#widgets) 8 | - [Widget composition](#widget-composition) 9 | - [Widget state](#widget-state) 10 | - [Images](#images) 11 | - [Network Image](#network-image) 12 | - [Asset Image](#asset-image) 13 | - [Buttons & Icons](#buttons--icons) 14 | - [Icons](#icons) 15 | - [Buttons](#buttons) 16 | - [Containers & Padding](#containers--padding) 17 | - [Rows & Columns](#rows--columns) 18 | - [Challenge](#challenge) 19 | 20 | ## Widgets 21 | 22 | Widgets are the building blocks of a Flutter app's user interface, and each widget is an immutable declaration of part of the user interface. Widgets are used to describe all aspects of a user interface, including physical aspects such as text and buttons to lay out effects like padding and alignment. 23 | 24 | ### Widget composition 25 | 26 | A widget can have child widgets in the form of attributes. Widgets relationships are hierarchical, and the parent widget can have multiple child widgets. Here is an example of a widget composition for this code. 27 | 28 | ```dart 29 | void main() { 30 | runApp(MaterialApp( 31 | home: Scaffold( 32 | appBar: AppBar( 33 | title: Text("My first app"), 34 | centerTitle: true, 35 | backgroundColor: Colors.amber[200], 36 | ), 37 | body: Center( 38 | child: Text( 39 | 'Hello World!', 40 | style: TextStyle( 41 | fontSize: 20.0, 42 | fontWeight: FontWeight.bold, 43 | letterSpacing: 2.0, 44 | color: Colors.grey[600], 45 | ), 46 | ), 47 | ), 48 | floatingActionButton: FloatingActionButton( 49 | onPressed: () {}, 50 | child: Text('click'), 51 | backgroundColor: Color(0xFF42A5F5), 52 | ), 53 | ), 54 | )); 55 | } 56 | ``` 57 | 58 |
59 | 60 | ![widgets](images/01-widgets-composition.png) 61 | 62 |
63 | 64 | Widget are able to be nested within each other to create complex user interfaces. For example, a `Scaffold` widget can contain an `AppBar`, a `Center` as a body, a `FloatingActionButton`, and more. 65 | 66 | ### Widget state 67 | 68 | Widgets can be stateless or stateful. Stateless widgets are immutable, meaning that their properties can't change - all values are final (used for static content). Stateful widgets maintain state that might change during the lifetime of the widget (used for dynamic content). 69 | 70 | For this module, we will focus on stateless widgets that provided by Flutter. 71 | 72 | ## Images 73 | 74 | Images can be displayed in Flutter using the `Image` widget. The `Image` widget can display images from the network or from the local assets. 75 | 76 | ### Network Image 77 | 78 | To display an image from the network, use the `NetworkImage` widget in the `Image` widget. Try changing the `body` of `Scaffold` to display an image from the network. 79 | 80 | ```dart 81 | body: Center( 82 | child: Image( 83 | image: NetworkImage('https://picsum.photos/200'), 84 | ), 85 | ), 86 | ``` 87 | 88 | or 89 | 90 | ```dart 91 | body: Center( 92 | child: Image.network('https://picsum.photos/200'), 93 | ), 94 | ``` 95 | 96 |
97 | 98 | ![network-image](images/02-images-network-image.png) 99 | 100 |
101 | 102 | ### Asset Image 103 | 104 | To display an image from asset directory, we need to create the `assets` directory in the root of the project and add the image file. Then, we need to modify the `pubspec.yaml` file to include the asset (below the commented instruction). 105 | 106 |
107 | 108 | ![assets](images/03-images-asset-image-asset-dir.png) 109 | 110 |
111 | 112 | ```yaml 113 | # To add assets to your application, add an assets section, like this: 114 | # assets: 115 | # - images/a_dot_burr.jpeg 116 | # - images/a_dot_ham.jpeg 117 | 118 | assets: 119 | - assets/ # add this line to include the assets directory 120 | - assets/legit-image.jpg # add this line to include specific image file 121 | ``` 122 | 123 | Then, we can display the image from the asset directory by using the `AssetImage` widget in the `Image` widget. 124 | 125 | ```dart 126 | body: Center( 127 | child: Image( 128 | image: AssetImage('assets/legit-image.jpg'), 129 | ), 130 | ), 131 | ``` 132 | 133 | or 134 | 135 | ```dart 136 | body: Center( 137 | child: Image.asset('assets/legit-image.jpg'), 138 | ), 139 | ``` 140 | 141 |
142 | 143 | ![asset-image](images/04-images-asset-image.png) 144 | 145 |
146 | 147 | Want to know more about asset image? [See the docs](https://docs.flutter.dev/ui/assets/assets-and-images) 148 | 149 | ## Buttons & Icons 150 | 151 | Buttons and icons are essential widgets in Flutter for user interaction. Flutter provides various types of buttons and icons that can be customized to suit the app's design. 152 | 153 | ### Icons 154 | 155 | Icons are visual symbols for representing actions, objects, or concepts. Flutter provides a wide range of icons that can be used in the app. To use an icon, we can use the `Icon` widget. Try changing the `body` of `Scaffold` to display an icon. 156 | 157 | ```dart 158 | body: Center( 159 | child: Icon( 160 | Icons.ac_unit, 161 | color: Colors.lightBlue, 162 | size: 50.0, 163 | ), 164 | ), 165 | ``` 166 | 167 |
168 | 169 | ![icon](images/05-icons.png) 170 | 171 |
172 | 173 | Tips: you can click `Ctrl` + `Space` to see the available icons or the attribute. 174 | 175 |
176 | 177 | ![icon-tips](images/06-icons-tips.png) 178 | 179 |
180 | 181 | ### Buttons 182 | 183 | Buttons are used to trigger an action when clicked. Flutter provides various types of buttons, such as `ElevatedButton`, `TextButton`, and `OutlinedButton`. To use a button, we can use the `ElevatedButton` widget. Try changing the `floatingActionButton` of `Scaffold` to display a button. 184 | 185 | ```dart 186 | floatingActionButton: ElevatedButton( 187 | onPressed: () {}, 188 | child: Text('click'), 189 | ), 190 | ``` 191 | 192 | Try using the `TextButton` and `OutlinedButton` widgets to see the different button styles. 193 | 194 |
195 | 196 | ![buttons](images/07-buttons-type.png) 197 | 198 |
199 | 200 | Buttons can be styled using the `style` property of the button widget. The `style` property can be used to set the button's text style, background color, and more. 201 | 202 | ```dart 203 | floatingActionButton: ElevatedButton( 204 | onPressed: () {}, 205 | child: Icon(Icons.add), 206 | style: ElevatedButton.styleFrom( 207 | backgroundColor: Colors.lightBlue, 208 | shape: CircleBorder(), 209 | padding: EdgeInsets.all(20.0), 210 | ), 211 | ), 212 | ``` 213 | 214 |
215 | 216 | ![buttons-style](images/08-buttons-style.png) 217 | 218 |
219 | 220 | ## Containers & Padding 221 | 222 | Containers are used to create a visual element in the app. Containers can be adjusted in terms of size, padding, margin, and more. Here is the illustration of the container with padding and margin. 223 | 224 |
225 | 226 | ![container-padding-margin](images/09-container-padding-margin.png) 227 | 228 |
229 | 230 | To create a container with padding and margin, we can use the `Container` widget with the `padding` and `margin` property. 231 | 232 | ```dart 233 | body: Container( 234 | padding: EdgeInsets.all(20.0), 235 | margin: EdgeInsets.all(20.0), 236 | color: Colors.amber, 237 | child: Text('This is container'), 238 | ), 239 | ``` 240 | 241 |
242 | 243 | ![container-padding-margin-result](images/10-container-padding-margin-illustration.png) 244 | 245 |
246 | 247 | Padding and margin property are using `EdgeInsets` class to set the padding and margin. The `EdgeInsets` class provides various methods to set the padding and margin, such as `all`, `only`, `symmetric`, and more. 248 | 249 | ```dart 250 | padding: EdgeInsets.all(20.0), // all sides 251 | padding: EdgeInsets.only(left: 20.0), // only left side 252 | padding: EdgeInsets.symmetric(horizontal: 20.0), // horizontal sides 253 | padding: EdgeInsets.fromLTRB(10.0, 20.0, 30.0, 40.0), // left, top, right, bottom 254 | ``` 255 | 256 | If we want to only add padding to the child widget, we can use the `Padding` widget instead of the `padding` property of the `Container` widget. 257 | 258 | ```dart 259 | body: Padding( 260 | padding: EdgeInsets.all(40.0), 261 | child: Text('Hello World!'), 262 | ), 263 | ``` 264 | 265 |
266 | 267 | ![padding-widget](images/11-padding-widget.png) 268 | 269 |
270 | 271 | ## Rows & Columns 272 | 273 | Rows and columns are used to arrange child widgets horizontally (row) or vertically (column). Rows and columns can be used to create complex layouts in the app. 274 | 275 | For easier demonstration, we will be using containers as child widgets for rows and columns. 276 | 277 | To create a row, we can use the `Row` widget with the `children` property. 278 | 279 | ```dart 280 | body: Container( 281 | decoration: BoxDecoration( 282 | border: Border.all(color: Colors.black), 283 | ), 284 | child: Row( 285 | mainAxisAlignment: MainAxisAlignment.center, 286 | crossAxisAlignment: CrossAxisAlignment.center, 287 | children: [ 288 | Container( 289 | decoration: BoxDecoration( 290 | border: Border.all(color: Colors.black), 291 | ), 292 | width: 100, 293 | height: 100, 294 | child: Text('Container 1'), 295 | ), 296 | Container( 297 | decoration: BoxDecoration( 298 | border: Border.all(color: Colors.black), 299 | ), 300 | width: 150, 301 | height: 150, 302 | child: Text('Container 2'), 303 | ), 304 | ], 305 | ), 306 | ), 307 | ``` 308 | 309 |
310 | 311 | ![row](images/12-row.png) 312 | 313 |
314 | 315 | To create a column, we can use the `Column` widget with the `children` property. 316 | 317 | ```dart 318 | body: Container( 319 | decoration: BoxDecoration( 320 | border: Border.all(color: Colors.black), 321 | ), 322 | child: Column( 323 | mainAxisAlignment: MainAxisAlignment.center, 324 | crossAxisAlignment: CrossAxisAlignment.center, 325 | children: [ 326 | Container( 327 | decoration: BoxDecoration( 328 | border: Border.all(color: Colors.black), 329 | ), 330 | width: 100, 331 | height: 100, 332 | child: Text('Container 1'), 333 | ), 334 | Container( 335 | decoration: BoxDecoration( 336 | border: Border.all(color: Colors.black), 337 | ), 338 | width: 150, 339 | height: 150, 340 | child: Text('Container 2'), 341 | ), 342 | ], 343 | ), 344 | ), 345 | ``` 346 | 347 |
348 | 349 | ![column](images/13-column.png) 350 | 351 |
352 | 353 | Attributes like `mainAxisAlignment` and `crossAxisAlignment` can be used to align the child widgets in the row or column. The `mainAxisAlignment` attribute is used to align the child widgets along the main axis (row for row, column for column), while the `crossAxisAlignment` attribute is used to align the child widgets along the cross axis (column for row, row for column). 354 | 355 | Try changing the `mainAxisAlignment` and `crossAxisAlignment` attributes to see the different alignments. 356 | 357 | Want to know more about rows and columns? [See the docs](https://docs.flutter.dev/ui/layout) 358 | 359 | ## Challenge 360 | 361 | Can you recreate this layout using the widgets we have learned so far? 362 | 363 |
364 | 365 | ![challenge](images/14-challenge.png) 366 | 367 |
368 | -------------------------------------------------------------------------------- /09. Firebase Auth/README.md: -------------------------------------------------------------------------------- 1 | # 08. Firebase Auth 2 | 3 | [Previous](/07.%20Awesome%20Notifications/) | [Main Page](/) | [Next](/09.%20Connecting%20API%20-%20Machine%20Learning%20Service/) 4 | 5 | ## Content Outline 6 | 7 | - [Local Setup](#local-setup) 8 | - [Create a Firebase Project](#create-a-firebase-project) 9 | - [Set Up Firebase Authentication](#set-up-firebases-authentication) 10 | - [Flutter Implementation](#flutter-implementation) 11 | - [User Management](#user-management) 12 | 13 | ## Local Setup 14 | 15 | To use Firebase service, you need to log in to your Firebase account in your terminal using Firebase CLI. Firebase CLI can be installed using the following command: 16 | 17 | ```bash 18 | npm install -g firebase-tools 19 | ``` 20 | 21 | or you can download it from the [Firebase CLI documentation](https://firebase.google.com/docs/cli). 22 | 23 | After that, log in to your Firebase account using the following command: 24 | 25 | ```bash 26 | firebase login 27 | ``` 28 | 29 | ## Create a Firebase Project 30 | 31 | Before you can use Firebase Authentication, you need to create a Firebase project. To do this, follow these steps: 32 | 33 | 1. Go to the [Firebase Console](https://console.firebase.google.com/). 34 | 2. Click on "Add project" and follow the prompts to create a new project. 35 | 36 |
37 | 38 | 39 |
40 | 41 | 3. Once your project is created, add Flutter as a platform. 42 | 43 |
44 | 45 | 46 | 47 |
48 | 49 | 4. Install `firebase_core` and `firebase_auth` packages in your Flutter project. 50 | 51 | ```bash 52 | flutter pub add firebase_core 53 | flutter pub add firebase_auth 54 | ``` 55 | 56 | 5. Change `main.dart` file to use the Firebase project. 57 | 58 | ```dart 59 | import 'package:firebase_core/firebase_core.dart'; 60 | import 'firebase_options.dart'; 61 | 62 | void main() async { 63 | WidgetsFlutterBinding.ensureInitialized(); 64 | await Firebase.initializeApp( 65 | options: DefaultFirebaseOptions.currentPlatform, 66 | ); 67 | runApp(const MyApp()); 68 | } 69 | ``` 70 | 71 | ## Set Up Firebases Authentication 72 | 73 | After creating a Firebase project, you need to set up Firebase Authentication. To do this, follow these steps: 74 | 75 | 1. Go to the [Firebase Console](https://console.firebase.google.com/). 76 | 77 | 2. Select your project. 78 | 79 | 3. Go to the "Authentication" section. 80 | 81 | 4. Click on "Get Started". 82 | 83 | 5. Click on "Sign-in method". 84 | 85 |
86 | 87 |
88 | 89 | 6. Enable Email/Password authentication. 90 | 91 |
92 | 93 | 94 |
95 | 96 | 7. Click on "Save". 97 | 98 | ## Flutter Implementation 99 | 100 | We will create a simple UI for authentication using Flutter. The UI will have three screens, one for login, one for registration, and one for account information. 101 | 102 | ### Login Screen 103 | 104 | To login into an authenticated user, we can use `signInWithEmailAndPassword()` function from Firebase Auth. 105 | 106 | Create a new file `login.dart` in the `lib/screens` directory and add the following code: 107 | 108 | ```dart 109 | import 'package:firebase_auth/firebase_auth.dart'; 110 | import 'package:flutter/material.dart'; 111 | 112 | class LoginScreen extends StatefulWidget { 113 | const LoginScreen({super.key}); 114 | 115 | @override 116 | State createState() => _LoginScreenState(); 117 | } 118 | 119 | class _LoginScreenState extends State { 120 | final _emailController = TextEditingController(); 121 | final _passwordController = TextEditingController(); 122 | 123 | bool _isLoading = false; 124 | String _errorCode = ""; 125 | 126 | void navigateRegister() { 127 | if (!context.mounted) return; 128 | Navigator.pushReplacementNamed(context, 'register'); 129 | } 130 | 131 | void navigateHome() { 132 | if (!context.mounted) return; 133 | Navigator.pushReplacementNamed(context, 'home'); 134 | } 135 | 136 | void signIn() async { 137 | setState(() { 138 | _isLoading = true; 139 | _errorCode = ""; 140 | }); 141 | 142 | try { 143 | await FirebaseAuth.instance.signInWithEmailAndPassword( 144 | email: _emailController.text, 145 | password: _passwordController.text, 146 | ); 147 | navigateHome(); 148 | } on FirebaseAuthException catch (e) { 149 | setState(() { 150 | _errorCode = e.code; 151 | }); 152 | } 153 | 154 | setState(() { 155 | _isLoading = false; 156 | }); 157 | } 158 | 159 | @override 160 | Widget build(BuildContext context) { 161 | return Scaffold( 162 | appBar: AppBar( 163 | title: const Text('Login'), 164 | centerTitle: true, 165 | ), 166 | body: Padding( 167 | padding: const EdgeInsets.all(16.0), 168 | child: Center( 169 | child: ListView( 170 | children: [ 171 | const SizedBox(height: 48), 172 | Icon(Icons.lock_outline, size: 100, color: Colors.blue[200]), 173 | const SizedBox(height: 48), 174 | TextField( 175 | controller: _emailController, 176 | decoration: const InputDecoration(label: Text('Email')), 177 | ), 178 | TextField( 179 | controller: _passwordController, 180 | obscureText: true, 181 | decoration: const InputDecoration(label: Text('Password')), 182 | ), 183 | const SizedBox(height: 24), 184 | _errorCode != "" 185 | ? Column( 186 | children: [Text(_errorCode), const SizedBox(height: 24)]) 187 | : const SizedBox(height: 0), 188 | OutlinedButton( 189 | onPressed: signIn, 190 | child: _isLoading 191 | ? const CircularProgressIndicator() 192 | : const Text('Login'), 193 | ), 194 | Row( 195 | mainAxisAlignment: MainAxisAlignment.center, 196 | children: [ 197 | const Text('Don\'t have an account?'), 198 | TextButton( 199 | onPressed: navigateRegister, 200 | child: const Text('Register'), 201 | ) 202 | ], 203 | ) 204 | ], 205 | ), 206 | ), 207 | ), 208 | ); 209 | } 210 | } 211 | ``` 212 | 213 | ### Registration Screen 214 | 215 | The registration screen will be the same as the login screen, but we will use `createUserWithEmailAndPassword()` function from Firebase Auth. 216 | 217 | Create a new file `register.dart` in the `lib/screens` directory and add the following code: 218 | 219 | ```dart 220 | import 'package:firebase_auth/firebase_auth.dart'; 221 | import 'package:flutter/material.dart'; 222 | 223 | class RegisterScreen extends StatefulWidget { 224 | const RegisterScreen({super.key}); 225 | 226 | @override 227 | State createState() => _RegisterScreenState(); 228 | } 229 | 230 | class _RegisterScreenState extends State { 231 | final _emailController = TextEditingController(); 232 | final _passwordController = TextEditingController(); 233 | 234 | bool _isLoading = false; 235 | String _errorCode = ""; 236 | 237 | void navigateLogin() { 238 | if (!context.mounted) return; 239 | Navigator.pushReplacementNamed(context, 'login'); 240 | } 241 | 242 | void navigateHome() { 243 | if (!context.mounted) return; 244 | Navigator.pushReplacementNamed(context, 'home'); 245 | } 246 | 247 | void register() async { 248 | setState(() { 249 | _isLoading = true; 250 | _errorCode = ""; 251 | }); 252 | 253 | try { 254 | await FirebaseAuth.instance.createUserWithEmailAndPassword( 255 | email: _emailController.text, 256 | password: _passwordController.text, 257 | ); 258 | navigateLogin(); 259 | } on FirebaseAuthException catch (e) { 260 | setState(() { 261 | _errorCode = e.code; 262 | }); 263 | } 264 | 265 | setState(() { 266 | _isLoading = false; 267 | }); 268 | } 269 | 270 | @override 271 | Widget build(BuildContext context) { 272 | return Scaffold( 273 | appBar: AppBar( 274 | title: const Text('Register'), 275 | centerTitle: true, 276 | ), 277 | body: Padding( 278 | padding: const EdgeInsets.all(16.0), 279 | child: Center( 280 | child: ListView( 281 | children: [ 282 | const SizedBox(height: 48), 283 | Icon(Icons.lock_outline, size: 100, color: Colors.blue[200]), 284 | const SizedBox(height: 48), 285 | TextField( 286 | controller: _emailController, 287 | decoration: const InputDecoration(label: Text('Email')), 288 | ), 289 | TextField( 290 | controller: _passwordController, 291 | obscureText: true, 292 | decoration: const InputDecoration(label: Text('Password')), 293 | ), 294 | const SizedBox(height: 24), 295 | _errorCode != "" 296 | ? Column( 297 | children: [Text(_errorCode), const SizedBox(height: 24)], 298 | ) 299 | : const SizedBox(height: 0), 300 | OutlinedButton( 301 | onPressed: register, 302 | child: _isLoading 303 | ? const CircularProgressIndicator() 304 | : const Text('Register'), 305 | ), 306 | Row( 307 | mainAxisAlignment: MainAxisAlignment.center, 308 | children: [ 309 | const Text('Already have an account?'), 310 | TextButton( 311 | onPressed: navigateLogin, 312 | child: const Text('Login'), 313 | ) 314 | ], 315 | ) 316 | ], 317 | ), 318 | ), 319 | ), 320 | ); 321 | } 322 | } 323 | ``` 324 | 325 | ### Account Information Screen 326 | 327 | To synchronize the authentication state, we can use `authStateChanges()` stream from Firebase Auth. This stream will notify us whenever the authentication state changes. To logout the authenticated user, we can use `signOut()` function from Firebase Auth. 328 | 329 | Create a new file `home.dart` in the `lib/screens` directory and add the following code: 330 | 331 | ```dart 332 | import 'package:auth_modul/screens/login.dart'; 333 | import 'package:firebase_auth/firebase_auth.dart'; 334 | import 'package:flutter/material.dart'; 335 | 336 | class HomeScreen extends StatelessWidget { 337 | const HomeScreen({super.key}); 338 | 339 | void logout(context) async { 340 | await FirebaseAuth.instance.signOut(); 341 | Navigator.pushReplacementNamed(context, 'login'); 342 | } 343 | 344 | @override 345 | Widget build(BuildContext context) { 346 | return StreamBuilder( 347 | stream: FirebaseAuth.instance.authStateChanges(), 348 | builder: (context, snapshot) { 349 | if (snapshot.hasData) { 350 | return Scaffold( 351 | appBar: AppBar( 352 | title: const Text('Account Information'), 353 | centerTitle: true, 354 | ), 355 | body: Center( 356 | child: Column( 357 | mainAxisAlignment: MainAxisAlignment.center, 358 | children: [ 359 | Text('Logged in as ${snapshot.data?.email}'), 360 | const SizedBox(height: 24), 361 | OutlinedButton( 362 | onPressed: () => logout(context), 363 | child: const Text('Logout'), 364 | ) 365 | ], 366 | ), 367 | ), 368 | ); 369 | } else { 370 | return const LoginScreen(); 371 | } 372 | }, 373 | ); 374 | } 375 | } 376 | ``` 377 | 378 | ### Main File 379 | 380 | Finally, we need to update the `main.dart` file to include the new screens. 381 | 382 | ```dart 383 | import 'package:auth_modul/screens/home.dart'; 384 | import 'package:auth_modul/screens/login.dart'; 385 | import 'package:auth_modul/screens/register.dart'; 386 | import 'package:flutter/material.dart'; 387 | import 'package:firebase_core/firebase_core.dart'; 388 | import 'firebase_options.dart'; 389 | 390 | void main() async { 391 | WidgetsFlutterBinding.ensureInitialized(); 392 | await Firebase.initializeApp( 393 | options: DefaultFirebaseOptions.currentPlatform, 394 | ); 395 | runApp(const MyApp()); 396 | } 397 | 398 | class MyApp extends StatelessWidget { 399 | const MyApp({super.key}); 400 | 401 | @override 402 | Widget build(BuildContext context) { 403 | return MaterialApp(initialRoute: 'login', routes: { 404 | 'home': (context) => const HomeScreen(), 405 | 'login': (context) => const LoginScreen(), 406 | 'register': (context) => const RegisterScreen(), 407 | }); 408 | } 409 | } 410 | ``` 411 | 412 | These are the result of the authentication module. You can now run the app and test the authentication functionality. 413 | 414 |
415 | 416 | 417 | 418 |
419 | 420 | ## User Management 421 | 422 | We can manage users using Firebase Console. To do this, follow these steps: 423 | 424 | 1. Go to the [Firebase Console](https://console.firebase.google.com/). 425 | 426 | 2. Select your project. 427 | 428 | 3. Go to the "Authentication" section. 429 | 430 | 4. Click on "Users". 431 | 432 | 5. You can add, edit, or delete users from this section. 433 | 434 |
435 | 436 |
437 | -------------------------------------------------------------------------------- /10. Awesome Notifications/README.md: -------------------------------------------------------------------------------- 1 | # 07. Awesome Notifications 2 | 3 | [Previous](/06.%20Firebase/) | [Main Page](/) | [Next](/08.%20Firebase%20Auth/) 4 | 5 | ## Mobile Notifications 6 | 7 | To use notifications in our app, we can use the `awesome_notifications` package. This package provides a simple and easy way to create and manage notifications in our app. 8 | 9 | ### Step 1: Add Dependency 10 | 11 | Add the `awesome_notifications` using the following command: 12 | 13 | ```bash 14 | flutter pub add awesome_notifications:^0.7.4+1 15 | ``` 16 | 17 | After adding the dependency, we need to modify the `android/app/build.gradle` file to include the following lines: 18 | 19 | ```groovy 20 | android { 21 | ... 22 | compileSdkVersion 34 // change the compileSdkVersion to 34 23 | ... 24 | 25 | defaultConfig { 26 | ... 27 | minSdkVersion 23 // change the minSdkVersion to 23 28 | targetSdkVersion 33 // change the targetSdkVersion to 33 29 | } 30 | } 31 | ``` 32 | 33 | ### Step 2: Configure Permissions 34 | 35 | To use notifications in our app, we need to configure the permissions in the `AndroidManifest.xml` file. Open the `android/app/src/main/AndroidManifest.xml` file and add the following lines: 36 | 37 | ```xml 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | ... 49 | ``` 50 | 51 | ### Step 3: Create Notification Service 52 | 53 | Create a new file called `notification_service.dart` in the `lib/services` folder. 54 | 55 | We will create a `NotificationService` class that will handle the initialization and display of notifications. 56 | 57 | Below is the code for initializing the notification service. 58 | 59 | ```dart 60 | static Future initializeNotification() async { 61 | // Initialize Awesome Notifications 62 | await AwesomeNotifications().initialize( 63 | null, 64 | [ 65 | NotificationChannel( 66 | channelGroupKey: 'basic_channel_group', 67 | channelKey: 'basic_channel', 68 | channelName: 'Basic notifications', 69 | channelDescription: 'Notification channel for basic tests', 70 | defaultColor: const Color(0xFF9D50DD), 71 | ledColor: Colors.white, 72 | importance: NotificationImportance.Max, 73 | channelShowBadge: true, 74 | playSound: true, 75 | criticalAlerts: true, 76 | ) 77 | ], 78 | channelGroups: [ 79 | NotificationChannelGroup( 80 | channelGroupKey: 'basic_channel_group', 81 | channelGroupName: 'Basic notifications group', 82 | ) 83 | ], 84 | debug: true, 85 | ); 86 | 87 | // Request notification permissions 88 | await AwesomeNotifications().isNotificationAllowed().then( 89 | (isAllowed) { 90 | if (!isAllowed) { 91 | AwesomeNotifications().requestPermissionToSendNotifications(); 92 | } 93 | }, 94 | ); 95 | 96 | // Set notification listeners 97 | await AwesomeNotifications().setListeners( 98 | onActionReceivedMethod: _onActionReceivedMethod, 99 | onNotificationCreatedMethod: _onNotificationCreateMethod, 100 | onNotificationDisplayedMethod: _onNotificationDisplayedMethod, 101 | onDismissActionReceivedMethod: _onDismissActionReceivedMethod, 102 | ); 103 | } 104 | 105 | // Listeners 106 | 107 | static Future _onNotificationCreateMethod( 108 | ReceivedNotification receivedNotification, 109 | ) async { 110 | debugPrint('Notification created: ${receivedNotification.title}'); 111 | } 112 | 113 | static Future _onNotificationDisplayedMethod( 114 | ReceivedNotification receivedNotification, 115 | ) async { 116 | debugPrint('Notification displayed: ${receivedNotification.title}'); 117 | } 118 | 119 | static Future _onDismissActionReceivedMethod( 120 | ReceivedNotification receivedNotification, 121 | ) async { 122 | debugPrint('Notification dismissed: ${receivedNotification.title}'); 123 | } 124 | 125 | static Future _onActionReceivedMethod( 126 | ReceivedNotification receivedNotification, 127 | ) async { 128 | debugPrint('Notification action received: ${receivedNotification.title}'); 129 | } 130 | ``` 131 | 132 | To create a notification, we will create a method called `createNotification` in the `NotificationService` class. 133 | 134 | ```dart 135 | static Future createNotification({ 136 | required final int id, 137 | required final String title, 138 | required final String body, 139 | final String? summary, 140 | final Map? payload, 141 | final ActionType actionType = ActionType.Default, 142 | final NotificationLayout notificationLayout = NotificationLayout.Default, 143 | final NotificationCategory? category, 144 | final String? bigPicture, 145 | final List? actionButtons, 146 | final bool scheduled = false, 147 | final Duration? interval, 148 | }) async { 149 | assert(!scheduled || (scheduled && interval != null)); 150 | 151 | await AwesomeNotifications().createNotification( 152 | content: NotificationContent( 153 | id: id, 154 | channelKey: 'basic_channel', 155 | title: title, 156 | body: body, 157 | actionType: actionType, 158 | notificationLayout: notificationLayout, 159 | summary: summary, 160 | category: category, 161 | payload: payload, 162 | bigPicture: bigPicture, 163 | ), 164 | actionButtons: actionButtons, 165 | schedule: scheduled 166 | ? NotificationInterval( 167 | interval: interval, 168 | timeZone: 169 | await AwesomeNotifications().getLocalTimeZoneIdentifier(), 170 | preciseAlarm: true, 171 | ) 172 | : null, 173 | ); 174 | } 175 | ``` 176 | 177 | ### Step 4: Create Home Screen 178 | 179 | Create a new file called `home_screen.dart` in the `lib/screens` folder. 180 | We will create a simple UI which consists of buttons to create notifications. 181 | 182 | ```dart 183 | import 'package:awesome_notifications/awesome_notifications.dart'; 184 | import 'package:flutter/material.dart'; 185 | import 'package:notification_demo/services/notification_service.dart'; 186 | 187 | class HomeScreen extends StatelessWidget { 188 | const HomeScreen({super.key}); 189 | 190 | @override 191 | Widget build(BuildContext context) { 192 | return Scaffold( 193 | appBar: AppBar( 194 | title: const Text('Home Page'), 195 | centerTitle: true, 196 | ), 197 | body: ListView( 198 | padding: const EdgeInsets.symmetric(horizontal: 20), 199 | children: [ 200 | // Placeholder for the notification buttons 201 | ], 202 | ), 203 | ); 204 | } 205 | } 206 | ``` 207 | 208 | These are the buttons we will create to test the notifications. 209 | 210 | #### Default Notification 211 | 212 | To create a default notification, we will create a button that will call the `createNotification` method in the `NotificationService` class. 213 | 214 | ```dart 215 | OutlinedButton( 216 | onPressed: () async { 217 | await NotificationService.createNotification( 218 | id: 1, 219 | title: 'Default Notification', 220 | body: 'This is the body of the notification', 221 | summary: 'Small summary', 222 | ); 223 | }, 224 | child: const Text('Default Notification'), 225 | ) 226 | ``` 227 | 228 | #### Notification with Summary 229 | 230 | To create a notification with a summary, we will create a button that will call the `createNotification` method in the `NotificationService` class and change the `notificationLayout` to `NotificationLayout.Inbox`. 231 | 232 | ```dart 233 | OutlinedButton( 234 | onPressed: () async { 235 | await NotificationService.createNotification( 236 | id: 2, 237 | title: 'Notification with Summary', 238 | body: 'This is the body of the notification', 239 | summary: 'Small summary', 240 | notificationLayout: NotificationLayout.Inbox, 241 | ); 242 | }, 243 | child: const Text('Notification with Summary'), 244 | ) 245 | ``` 246 | 247 | #### Progress Bar Notification 248 | 249 | To create a progress bar notification, we will create a button that will call the `createNotification` method in the `NotificationService` class and change the `notificationLayout` to `NotificationLayout.ProgressBar`. 250 | 251 | ```dart 252 | OutlinedButton( 253 | onPressed: () async { 254 | await NotificationService.createNotification( 255 | id: 3, 256 | title: 'Progress Bar Notification', 257 | body: 'This is the body of the notification', 258 | summary: 'Small summary', 259 | notificationLayout: NotificationLayout.ProgressBar, 260 | ); 261 | }, 262 | child: const Text('Progress Bar Notification'), 263 | ) 264 | ``` 265 | 266 | #### Message Notification 267 | 268 | To create a message notification, we will create a button that will call the `createNotification` method in the `NotificationService` class and change the `notificationLayout` to `NotificationLayout.Messaging`. 269 | 270 | ```dart 271 | OutlinedButton( 272 | onPressed: () async { 273 | await NotificationService.createNotification( 274 | id: 4, 275 | title: 'Message Notification', 276 | body: 'This is the body of the notification', 277 | summary: 'Small summary', 278 | notificationLayout: NotificationLayout.Messaging, 279 | ); 280 | }, 281 | child: const Text('Message Notification'), 282 | ) 283 | ``` 284 | 285 | #### Big Image Notification 286 | 287 | To create a big image notification, we will create a button that will call the `createNotification` method in the `NotificationService` class and change the `notificationLayout` to `NotificationLayout.BigPicture`. 288 | 289 | ```dart 290 | OutlinedButton( 291 | onPressed: () async { 292 | await NotificationService.createNotification( 293 | id: 5, 294 | title: 'Big Image Notification', 295 | body: 'This is the body of the notification', 296 | summary: 'Small summary', 297 | notificationLayout: NotificationLayout.BigPicture, 298 | bigPicture: 'https://picsum.photos/300/200', 299 | ); 300 | }, 301 | child: const Text('Big Image Notification'), 302 | ) 303 | ``` 304 | 305 | #### Action Button Notification 306 | 307 | To create a notification with action buttons, we will create a button that will call the `createNotification` method in the `NotificationService` class and add action buttons to the notification. 308 | 309 | ```dart 310 | OutlinedButton( 311 | onPressed: () async { 312 | await NotificationService.createNotification( 313 | id: 5, 314 | title: 'Action Button Notification', 315 | body: 'This is the body of the notification', 316 | payload: {'navigate': 'true'}, 317 | actionButtons: [ 318 | NotificationActionButton( 319 | key: 'action_button', 320 | label: 'Click me', 321 | actionType: ActionType.Default, 322 | ) 323 | ], 324 | ); 325 | }, 326 | child: const Text('Action Button Notification'), 327 | ) 328 | ``` 329 | 330 | We need to handle the action button in the `_onActionReceivedMethod` method in the `NotificationService` class. Change the method to the following: 331 | 332 | ```dart 333 | static Future _onActionReceivedMethod( 334 | ReceivedNotification receivedNotification, 335 | ) async { 336 | debugPrint('Notification action received'); 337 | final payload = receivedNotification.payload; 338 | if (payload == null) return; 339 | if (payload['navigate'] == 'true') { 340 | debugPrint(MyApp.navigatorKey.currentContext.toString()); 341 | Navigator.push( 342 | MyApp.navigatorKey.currentContext!, 343 | MaterialPageRoute( 344 | builder: (_) => const SecondScreen(), 345 | ), 346 | ); 347 | } 348 | } 349 | ``` 350 | 351 | We also need to add the `second_screen.dart` file to handle the navigation. Create a new file called `second_screen.dart` in the `lib/screens` folder. 352 | 353 | ```dart 354 | import 'package:flutter/material.dart'; 355 | 356 | class SecondScreen extends StatelessWidget { 357 | const SecondScreen({super.key}); 358 | 359 | @override 360 | Widget build(BuildContext context) { 361 | return Scaffold( 362 | appBar: AppBar( 363 | title: const Text('Second Screen'), 364 | centerTitle: true, 365 | ), 366 | body: Center( 367 | child: Column( 368 | children: [ 369 | const Text('This is the second screen from the notification!'), 370 | const SizedBox(height: 20), 371 | OutlinedButton( 372 | onPressed: () { 373 | Navigator.pop(context); 374 | }, 375 | child: const Text('Go to Home'), 376 | ), 377 | ], 378 | ), 379 | ), 380 | ); 381 | } 382 | } 383 | ``` 384 | 385 | #### Scheduled Notification 386 | 387 | To create a scheduled notification, we will create a button that will call the `createNotification` method in the `NotificationService` class and set the `scheduled` parameter to `true` and the `interval` parameter to the desired interval. 388 | 389 | ```dart 390 | OutlinedButton( 391 | onPressed: () async { 392 | await NotificationService.createNotification( 393 | id: 5, 394 | title: 'Scheduled Notification', 395 | body: 'This is the body of the notification', 396 | scheduled: true, 397 | interval: 5, 398 | ); 399 | }, 400 | child: const Text('Scheduled Notification'), 401 | ) 402 | ``` 403 | 404 | ### Step 5: Main Function 405 | 406 | Finally, we need to modify the `main.dart` file to initialize the notification service and run the app. 407 | 408 | ```dart 409 | import 'package:flutter/material.dart'; 410 | import 'package:notification_app/screens/home_screen.dart'; 411 | import 'package:notification_app/screens/second_screen.dart'; 412 | import 'package:notification_app/services/notification_service.dart'; 413 | 414 | void main() async { 415 | WidgetsFlutterBinding.ensureInitialized(); 416 | await NotificationService.initializeNotification(); 417 | 418 | runApp(const MyApp()); 419 | } 420 | 421 | class MyApp extends StatelessWidget { 422 | const MyApp({super.key}); 423 | 424 | static GlobalKey navigatorKey = GlobalKey(); 425 | 426 | // This widget is the root of your application. 427 | @override 428 | Widget build(BuildContext context) { 429 | return MaterialApp( 430 | title: 'Notification Demo', 431 | routes: { 432 | 'home': (context) => const HomeScreen(), 433 | 'second': (context) => const SecondScreen(), 434 | }, 435 | initialRoute: 'home', 436 | navigatorKey: navigatorKey, 437 | ); 438 | } 439 | } 440 | ``` 441 | 442 | Here are the result of the notifications we created: 443 | 444 |
445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 |
454 | -------------------------------------------------------------------------------- /05. Form/README.md: -------------------------------------------------------------------------------- 1 | # 05. Form 2 | 3 | [Previous](/04.%20Stateful%20and%20Starting%20the%20World%20Time%20App/) | [Main Page](/) | [Next](/06.%20Firebase/) 4 | 5 | ## Content Outline 6 | 7 | - [Form](#form) 8 | - [TextField](#textfield) 9 | - [TextEditingController](#Texteditingcontroller) 10 | - [Form Widget](#form-widget) 11 | - [TextFormField](#textformfield) 12 | - [Another Example Project](#another-example-project) 13 | 14 | ## Form 15 | 16 | Form is used for collecting user input. There are two ways to make a form in Flutter by using `TextEditingController` or using `Form` Widget. 17 | 18 | ### TextField 19 | 20 | `TextField` is a Material Design widget to accept user input in the form of text by using hardware keyboard or with an onscreen keyboard. The `onChanged` callback is called whenever the user changes the text in the field. If the user indicates that they are done typing in the field, the `TextField` calls the `onSubmitted` callback. 21 | 22 | `TextField` has a `decoration` property to give the text field a label or an icon. To control the text that is displayed in the text field, we need to use a `controller`. 23 | 24 | ```dart 25 | TextField( 26 | controller: _controller, 27 | decoration: InputDecoration(label: Text("Here is your label")), 28 | ), 29 | ``` 30 | 31 | ### TextEditingController 32 | 33 | `TextEditingController` is a controller used for an editable text field. When the text field updates value from the user, the controller norifies its listeners and take the text from the text field. 34 | 35 | ```dart 36 | TextEditingController _controller = TextEditingController() 37 | ``` 38 | 39 | Here is the example to create form using `TextField` and `TextEditingController`. 40 | 41 | ```dart 42 | class _FormImageState extends State { 43 | String answer = ''; 44 | TextEditingController _controller = TextEditingController(); 45 | @override 46 | Widget build(BuildContext context) { 47 | return Container( 48 | child: Column( 49 | children: [ 50 | Padding( 51 | padding: const EdgeInsets.all(20.0), 52 | child: Column( 53 | children: [ 54 | TextField( 55 | controller: _controller, 56 | decoration: InputDecoration(label: Text("Enter your answer")), 57 | ), 58 | ElevatedButton(onPressed: () { 59 | setState(() { 60 | answer = _controller.text; 61 | }); 62 | 63 | }, child: Text("Answer")) 64 | ], 65 | ), 66 | ), 67 | Container( 68 | width: MediaQuery.of(context).size.width, 69 | margin: EdgeInsets.fromLTRB(20.0, 5.0, 20.0, 10.0), 70 | padding: EdgeInsets.all(20.0), 71 | color: Colors.yellow[200], 72 | child: Text("Your answer: $answer")) 73 | 74 | ], 75 | ) 76 | ); 77 | } 78 | } 79 | ``` 80 | 81 | Explanation: 82 | 83 | - We have one `TextField` to put our answer and we assigned it into `_controller` as `TextEditingController` 84 | - When we clicked the `ElevatedButton`, the text value from `TextField` by using its controller will be assigned into variable `answer`. 85 | 86 | **Full code** 87 | 88 | ```dart 89 | import 'package:flutter/material.dart'; 90 | 91 | void main() { 92 | runApp(const MyApp()); 93 | } 94 | 95 | class MyApp extends StatelessWidget { 96 | const MyApp({super.key}); 97 | 98 | // This widget is the root of your application. 99 | @override 100 | Widget build(BuildContext context) { 101 | return MaterialApp( 102 | title: 'Flutter Demo', 103 | theme: ThemeData( 104 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), 105 | useMaterial3: true, 106 | ), 107 | home: const RowColumnPage(), 108 | ); 109 | } 110 | } 111 | 112 | class RowColumnPage extends StatelessWidget { 113 | const RowColumnPage({Key? key}) : super(key: key); 114 | 115 | 116 | @override 117 | Widget build(BuildContext context) { 118 | MediaQueryData mediaQueryData = MediaQuery.of(context); 119 | double screenWidth = mediaQueryData.size.width; 120 | double screenHeight = mediaQueryData.size.height; 121 | return Scaffold( 122 | appBar: AppBar( 123 | title: const Text( 124 | 'My First App', 125 | style: TextStyle(color: Colors.black), 126 | ), 127 | backgroundColor: Colors.orange[200], 128 | centerTitle: true, 129 | ), 130 | body: Column( 131 | crossAxisAlignment: CrossAxisAlignment.center, 132 | mainAxisAlignment: MainAxisAlignment.center, 133 | children: [ 134 | Container( 135 | child: AspectRatio( 136 | aspectRatio: 1.0, 137 | child: Container( 138 | width: MediaQuery.of(context).size.width, 139 | margin: EdgeInsets.fromLTRB(20.0, 5.0, 20.0, 10.0), 140 | padding: EdgeInsets.all(20.0), 141 | color: Colors.lightBlue[100], 142 | child: Center( 143 | child: Image.network( 144 | 'https://picsum.photos/200', 145 | fit: BoxFit.cover, 146 | width: 500, 147 | ), 148 | ), 149 | ), 150 | ), 151 | ), 152 | Container( 153 | width: MediaQuery.of(context).size.width, 154 | margin: EdgeInsets.fromLTRB(20.0, 5.0, 20.0, 10.0), 155 | padding: EdgeInsets.all(20.0), 156 | color: Colors.pink[200], 157 | child: Text('What image is that', style: TextStyle(fontSize: 16)), 158 | ), 159 | FormImage(), 160 | // CounterCard(), 161 | ] 162 | ), 163 | ); 164 | } 165 | } 166 | 167 | class FormImage extends StatefulWidget { 168 | const FormImage({super.key}); 169 | 170 | @override 171 | State createState() => _FormImageState(); 172 | } 173 | 174 | class _FormImageState extends State { 175 | String answer = ''; 176 | TextEditingController _controller = TextEditingController(); 177 | @override 178 | Widget build(BuildContext context) { 179 | return Container( 180 | child: Column( 181 | children: [ 182 | Padding( 183 | padding: const EdgeInsets.all(20.0), 184 | child: Column( 185 | children: [ 186 | TextField( 187 | controller: _controller, 188 | decoration: InputDecoration(label: Text("Enter your answer")), 189 | ), 190 | ElevatedButton(onPressed: () { 191 | setState(() { 192 | answer = _controller.text; 193 | }); 194 | 195 | }, child: Text("Answer")) 196 | ], 197 | ), 198 | ), 199 | Container( 200 | width: MediaQuery.of(context).size.width, 201 | margin: EdgeInsets.fromLTRB(20.0, 5.0, 20.0, 10.0), 202 | padding: EdgeInsets.all(20.0), 203 | color: Colors.yellow[200], 204 | child: Text("Your answer: $answer")) 205 | 206 | ], 207 | ) 208 | ); 209 | } 210 | } 211 | 212 | ``` 213 | 214 | ![img](images/controller.png) 215 | 216 | ## Form Widget 217 | 218 | Flutter provides Form widget as an optional container for grouping together multiple form field widgets. It allows to add validation on text fields. There are some properties of Form widget. 219 | 220 | 1. **key**: A GlobalKey that uniquely identifies the Form. We can use this key to interact with the form, such as validating, resetting, or saving its state. 221 | 2. **child**: The child widget that contains the form fields 222 | 3. **autoValidateMode**: An enum that specifies when the form should automatically validate its fields. 223 | 224 | There are also some methods of Form Widget. 225 | 226 | 1. **validate()**: This method is used to trigger the validation of all the form fields within the Form. It returns true if all fields are valid, otherwise false. You can use it to check the overall validity of the form before submitting it. 227 | 2. **save()**: This method is used to save the current values of all form fields. It invokes the onSaved callback for each field. Typically, this method is called after validation succeeds. 228 | 3. **reset()**: Resets the form to its initial state, clearing any user-entered data. 229 | currentState: A getter that returns the current FormState associated with the Form. 230 | 231 | ## TextFormField 232 | 233 | `TextFormField` is a FormField that contains a `TextField`. It builds on `TextField` by integrating validation, input handling, and form state management. It is commonly used inside a `Form` widget to handle user input efficiently. 234 | 235 | `TextFormField` has some methods including: 236 | 237 | 1. `onSaved`: This method is used whenever user saves the form. 238 | 2. `validator`: This method is used to validate the text value entered by the user. 239 | 240 | Here is the example of validator method: 241 | 242 | ```dart 243 | TextFormField( 244 | decoration: InputDecoration(label: Text("Here is your label")), 245 | // The validator receives the text that the user has entered. 246 | validator: (value) { 247 | if (value == null || value.isEmpty) { 248 | return 'Please enter some text'; 249 | } 250 | return null; 251 | }, 252 | onSaved: (value) => {}, 253 | ), 254 | ``` 255 | 256 | Explanation: 257 | 258 | - Validator checks whether the value is empty or not. 259 | 260 | Let's replace `TextEditingController` and `TextField` widgets with `Form` and `TextFormField` widgets from the app before. 261 | 262 | ```dart 263 | class _FormImageState extends State { 264 | final _formKey = GlobalKey(); 265 | String _answer = ''; 266 | @override 267 | Widget build(BuildContext context) { 268 | return Container( 269 | child: Column( 270 | children: [ 271 | Padding( 272 | padding: const EdgeInsets.all(20.0), 273 | child: Form( 274 | key: _formKey, 275 | child: Column( 276 | children: [ 277 | TextFormField( 278 | decoration: InputDecoration(label: Text("Enter your answer")), 279 | validator: (value) => value!.isEmpty ? 'Please answer' : null, 280 | onSaved: (value) => setState(() { 281 | _answer = value!; 282 | }), 283 | ), 284 | ElevatedButton(onPressed: () { 285 | if (_formKey.currentState!.validate()) { 286 | _formKey.currentState!.save(); 287 | _formKey.currentState!.reset(); 288 | } 289 | 290 | }, child: Text("Answer")) 291 | ], 292 | ), 293 | ), 294 | ), 295 | Container( 296 | width: MediaQuery.of(context).size.width, 297 | margin: EdgeInsets.fromLTRB(20.0, 5.0, 20.0, 10.0), 298 | padding: EdgeInsets.all(20.0), 299 | color: Colors.yellow[200], 300 | child: Text("Your answer: $_answer")) 301 | 302 | ], 303 | ) 304 | ); 305 | } 306 | } 307 | ``` 308 | 309 | Explanation: 310 | 311 | - `_formKey` is used as an identifier for our form. It can be used to check to validate, save, and reset at the save button 312 | - `TextFormField` can automatically save the answer value to its variable by using `onSaved` method. 313 | 314 | **Full code** 315 | 316 | ```dart 317 | import 'package:flutter/material.dart'; 318 | 319 | void main() { 320 | runApp(const MyApp()); 321 | } 322 | 323 | class MyApp extends StatelessWidget { 324 | const MyApp({super.key}); 325 | 326 | // This widget is the root of your application. 327 | @override 328 | Widget build(BuildContext context) { 329 | return MaterialApp( 330 | title: 'Flutter Demo', 331 | theme: ThemeData( 332 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), 333 | useMaterial3: true, 334 | ), 335 | home: const RowColumnPage(), 336 | ); 337 | } 338 | } 339 | 340 | class RowColumnPage extends StatelessWidget { 341 | const RowColumnPage({Key? key}) : super(key: key); 342 | 343 | 344 | @override 345 | Widget build(BuildContext context) { 346 | MediaQueryData mediaQueryData = MediaQuery.of(context); 347 | double screenWidth = mediaQueryData.size.width; 348 | double screenHeight = mediaQueryData.size.height; 349 | return Scaffold( 350 | appBar: AppBar( 351 | title: const Text( 352 | 'My First App', 353 | style: TextStyle(color: Colors.black), 354 | ), 355 | backgroundColor: Colors.orange[200], 356 | centerTitle: true, 357 | ), 358 | body: Column( 359 | crossAxisAlignment: CrossAxisAlignment.center, 360 | mainAxisAlignment: MainAxisAlignment.center, 361 | children: [ 362 | Container( 363 | child: AspectRatio( 364 | aspectRatio: 1.0, 365 | child: Container( 366 | width: MediaQuery.of(context).size.width, 367 | margin: EdgeInsets.fromLTRB(20.0, 5.0, 20.0, 10.0), 368 | padding: EdgeInsets.all(20.0), 369 | color: Colors.lightBlue[100], 370 | child: Center( 371 | child: Image.network( 372 | 'https://picsum.photos/200', 373 | fit: BoxFit.cover, 374 | width: 500, 375 | ), 376 | ), 377 | ), 378 | ), 379 | ), 380 | Container( 381 | width: MediaQuery.of(context).size.width, 382 | margin: EdgeInsets.fromLTRB(20.0, 5.0, 20.0, 10.0), 383 | padding: EdgeInsets.all(20.0), 384 | color: Colors.pink[200], 385 | child: Text('What image is that', style: TextStyle(fontSize: 16)), 386 | ), 387 | FormImage(), 388 | // CounterCard(), 389 | ] 390 | ), 391 | ); 392 | } 393 | } 394 | 395 | class FormImage extends StatefulWidget { 396 | const FormImage({super.key}); 397 | 398 | @override 399 | State createState() => _FormImageState(); 400 | } 401 | 402 | class _FormImageState extends State { 403 | final _formKey = GlobalKey(); 404 | String _answer = ''; 405 | @override 406 | Widget build(BuildContext context) { 407 | return Container( 408 | child: Column( 409 | children: [ 410 | Padding( 411 | padding: const EdgeInsets.all(20.0), 412 | child: Form( 413 | key: _formKey, 414 | child: Column( 415 | children: [ 416 | TextFormField( 417 | decoration: InputDecoration(label: Text("Enter your answer")), 418 | validator: (value) => value!.isEmpty ? 'Please answer' : null, 419 | onSaved: (value) => setState(() { 420 | _answer = value!; 421 | }), 422 | ), 423 | ElevatedButton(onPressed: () { 424 | if (_formKey.currentState!.validate()) { 425 | _formKey.currentState!.save(); 426 | _formKey.currentState!.reset(); 427 | } 428 | 429 | }, child: Text("Answer")) 430 | ], 431 | ), 432 | ), 433 | ), 434 | Container( 435 | width: MediaQuery.of(context).size.width, 436 | margin: EdgeInsets.fromLTRB(20.0, 5.0, 20.0, 10.0), 437 | padding: EdgeInsets.all(20.0), 438 | color: Colors.yellow[200], 439 | child: Text("Your answer: $_answer")) 440 | 441 | ], 442 | ) 443 | ); 444 | } 445 | } 446 | ``` 447 | 448 | ![img](images/form.png) 449 | 450 | ## Another Example Project 451 | 452 | Here are some examples of projects for using forms. 453 | 454 | - 455 | - 456 | -------------------------------------------------------------------------------- /08. Firebase/README.md: -------------------------------------------------------------------------------- 1 | # 06. Firebase 2 | 3 | [Previous](/05.%20Form/) | [Main Page](/) | [Next](/07.%20Awesome%20Notifications/) 4 | 5 | ## 🔥 Setting Up Firebase Console for Your Flutter App 6 | 7 | Before you can connect your Flutter app to Firebase, you need to set up your project on the Firebase Console. This process is very straightforward, you’ll only need a Google account. 8 | 9 | --- 10 | 11 | ### 🟢 Step 1: Log In to Firebase 12 | 13 | To begin, open your web browser and head to the Firebase Console by visiting: 14 | 15 | 👉 [https://console.firebase.google.com](https://console.firebase.google.com) 16 | 17 | Once the page loads, log in using your Google account. If you're already signed into Gmail or other Google services, you’ll probably be logged in automatically. Otherwise, just enter your email and password to continue. 18 | 19 | --- 20 | 21 | ### 🟢 Step 2: Create a New Firebase Project 22 | 23 | After logging in, you’ll see the Firebase Dashboard. Find the button that says **“Add project”** or **“Create project”** and click on it to begin setting up your new project. 24 | 25 |

26 | 27 |

28 | 29 | You’ll be asked to enter a **name** for your project — this can be anything you like, such as `flutter-notes-app`. 30 | 31 |

32 | 33 |

34 | 35 | On the next screen, you might see options to enable **Google Analytics** and **Gemini**. For now, you can skip these by turning them off. Disabling Google Analytics will make the setup process quicker and simpler. 36 | 37 |

38 | 39 | 40 |

41 | 42 | --- 43 | 44 | ### 🟢 Step 3: Enable Firestore Database 45 | 46 | Now that your project is created, it’s time to set up the database. In the left-hand sidebar, click on **Build**, and then select **Firestore Database** from the dropdown menu. 47 | 48 |

49 | 50 |

51 | 52 | Once you’re on the Firestore page, you’ll see a button that says **“Create database.”** Click on it to begin. 53 | 54 |

55 | 56 |

57 | 58 | --- 59 | 60 | ### 🟢 Step 4: Choose the Firestore Location 61 | 62 | Next, Firebase will ask you to choose a location for your Firestore database. This location determines where your data is stored. For the best performance, since we're in Southeast Asia, choose **Singapore** or **Jakarta** as the region. 63 | 64 |

65 | 66 |

67 | 68 | --- 69 | 70 | ### 🟢 Step 5: Set Security Rules for Firestore 71 | 72 | Once your database is created, you’ll be taken to the **Security Rules** screen. Firebase will suggest using **Production Mode**, which means the database will start with stricter access by default. This is a good choice for safety, and you can change the rules later when needed. 73 | 74 |

75 | 76 |

77 | 78 | To allow your app to start writing to the database during development, you’ll need to change the default rules. Click on the **“Rules”** tab, and you’ll see a block of code like this: 79 | 80 | ```js 81 | rules_version = '2'; 82 | service cloud.firestore { 83 | match /databases/{database}/documents { 84 | match /{document=**} { 85 | allow read, write: if false; 86 | } 87 | } 88 | } 89 | ``` 90 | 91 | Here’s what to do: 92 | 93 | - Change `if false` to `if true`. This will temporarily allow full access while you're building and testing your app. 94 | 95 | ```js 96 | rules_version = '2'; 97 | service cloud.firestore { 98 | match /databases/{database}/documents { 99 | match /{document=**} { 100 | allow read, write: if true; 101 | } 102 | } 103 | } 104 | ``` 105 | 106 | After editing the rules, click **“Publish”** to save the changes. You might see a warning that this setting makes your database public. That’s okay for now while we’re still learning. 107 | 108 |

109 | 110 |

111 | 112 | --- 113 | 114 | ## 🤖 Connecting Flutter to Firebase (Android Studio Setup) 115 | 116 | Now that you’ve finished setting up your Firebase project, it’s time to connect it to your Flutter app. We’ll do this in a few steps: installing the right tools, linking your Firebase project, and adding the necessary packages to your Flutter project. 117 | 118 | --- 119 | 120 | ### 🛠 Step 1: Install Firebase CLI & FlutterFire CLI 121 | 122 | To get started, you’ll need to install some tools so that Flutter can connect to Firebase. Open **Terminal** inside your IDE, while opening your Flutter project. Type these commands one by one: 123 | 124 | ```bash 125 | npm install -g firebase-tools 126 | ``` 127 | 128 | This installs Firebase CLI globally using Node.js. You only need to do this once. 129 | 130 | Next, log into Firebase: 131 | 132 | ```bash 133 | firebase login 134 | ``` 135 | 136 | After you run this, a browser window will pop up asking you to sign in to your Google account. This is the same account you used to set up your Firebase project earlier. Then, activate the FlutterFire CLI: 137 | 138 | ```bash 139 | flutter pub global activate flutterfire_cli 140 | ``` 141 | 142 | And finally, link your Flutter app to the Firebase project using: 143 | 144 | ```bash 145 | flutterfire configure 146 | ``` 147 | 148 | > 💡 **Tip**: When running `flutterfire configure`, it will ask you to choose a Firebase project from your account. Make sure to pick the same project you created in the Firebase Console. 149 | 150 | Once this is done, FlutterFire will automatically generate a file called `firebase_options.dart` in your `lib` folder. This file contains the config info needed to initialize Firebase in your app. 151 | 152 | --- 153 | 154 | ### 📦 Step 2: Add Firebase Packages to Flutter 155 | 156 | Now let’s add the necessary Firebase packages to your project. 157 | 158 | In your terminal, run the following commands: 159 | 160 | ```bash 161 | flutter pub add firebase_core 162 | flutter pub add cloud_firestore 163 | ``` 164 | 165 | These two packages are essential: 166 | 167 | - `firebase_core` helps initialize Firebase in your Flutter app 168 | - `cloud_firestore` lets your app read/write data from Firestore 169 | 170 | After adding them, also make sure to run: 171 | 172 | ```bash 173 | flutter pub get 174 | ``` 175 | 176 | This will make sure the packages are properly downloaded and linked to your app. 177 | 178 | --- 179 | 180 | ### ⚠️ Step 3: Fix Android NDK Version (If You See an Error) 181 | 182 | Sometimes, you might see an error related to the **NDK version** when you build the app on Android. If that happens, here’s how to fix it. Go to this file in your Flutter project: 183 | 184 | ``` 185 | android/build.gradle.kts 186 | ``` 187 | 188 | Or sometimes inside: 189 | 190 | ``` 191 | android/app/build.gradle.kts 192 | ``` 193 | 194 | Inside the `android` block, specify the NDK version like this: 195 | 196 | ```kotlin 197 | android { 198 | ndkVersion = "27.0.12077973" 199 | 200 | // ... other settings 201 | } 202 | ``` 203 | 204 | Save the file, and try running your app again. 205 | 206 | --- 207 | 208 | ## 🧩 Writing Simple CRUD with Flutter + Firebase 209 | 210 |
211 | 212 | 213 | Watch the tutorial 214 | 215 | 216 |

217 | 📺 218 | Watch the full reference tutorial on YouTube 219 | 220 |

221 | 222 |
223 | 224 | Once our project is connected to Firebase, we can start writing the actual code for the app. In this section, we’re going to build a simple note-taking app using Flutter and Firebase Firestore. The app will let users add, view, edit, and delete short notes, and all of this will be saved to the cloud using Firebase. Thanks to Firestore’s real-time database updates, changes should appear instantly. 225 | 226 | This project is made up of three main Dart files: 227 | 228 | - `main.dart` → App entry point and Firebase setup 229 | - `home_page.dart` → The UI and logic for interacting with notes 230 | - `firestore.dart` → The service class that handles Firestore operations 231 | 232 | --- 233 | 234 | ### 🚀 `main.dart`: Setting Up the App 235 | 236 | This is where your Flutter app starts running. The most important part here is **initializing Firebase** properly before rendering any UI. 237 | 238 | ```dart 239 | void main() async { 240 | WidgetsFlutterBinding.ensureInitialized(); 241 | await Firebase.initializeApp( 242 | options: DefaultFirebaseOptions.currentPlatform, 243 | ); 244 | runApp(const MyApp()); 245 | } 246 | ``` 247 | 248 | - `WidgetsFlutterBinding.ensureInitialized();` makes sure that Flutter is ready before Firebase initializes. 249 | - `Firebase.initializeApp(...)` connects our app to Firebase using settings from `firebase_options.dart` (this file is auto-generated when you run `flutterfire configure`). 250 | - Then we launch the app with `runApp`. 251 | 252 | The UI starts with this simple widget: 253 | 254 | ```dart 255 | class MyApp extends StatelessWidget { 256 | @override 257 | Widget build(BuildContext context) { 258 | return MaterialApp( 259 | debugShowCheckedModeBanner: false, 260 | home: HomePage(), 261 | ); 262 | } 263 | } 264 | ``` 265 | 266 | We’re using `HomePage()` as the first screen, which is where our notes will show up. 267 | 268 | --- 269 | 270 | ### 🏠 `home_page.dart`: The Notes UI 271 | 272 | This is the main page where users can add, update, and delete notes. Let’s look at the key parts. 273 | 274 | --- 275 | 276 | #### 🔗 Connecting to Firestore 277 | 278 | First, we create an instance of our service class: 279 | 280 | ```dart 281 | final FirestoreService firestoreService = FirestoreService(); 282 | ``` 283 | 284 | We’ll use this to call `addNote`, `updateNote`, and `deleteNote` methods later. 285 | 286 | --- 287 | 288 | #### ✍️ The Add/Update Dialog 289 | 290 | Whenever the user wants to add or update a note, we open a dialog box: 291 | 292 | ```dart 293 | void openNoteBox({String? docID, String? existingText}) { 294 | textController.text = existingText ?? ''; 295 | 296 | showDialog( 297 | context: context, 298 | builder: (context) => AlertDialog( 299 | title: Text(docID == null ? 'Add Note' : 'Update Note'), 300 | content: Form( 301 | key: _formKey, 302 | child: TextFormField( 303 | controller: textController, 304 | autofocus: true, 305 | decoration: InputDecoration(hintText: 'Enter your note here'), 306 | validator: (value) { 307 | if (value == null || value.trim().isEmpty) { 308 | return 'Please enter some text'; 309 | } 310 | return null; 311 | }, 312 | ), 313 | ), 314 | actions: [ 315 | ElevatedButton( 316 | onPressed: () { 317 | if (_formKey.currentState!.validate()) { 318 | final text = textController.text.trim(); 319 | Navigator.pop(context); 320 | 321 | // Decide whether to add or update 322 | if (docID == null) { 323 | firestoreService.addNote(text); 324 | } else { 325 | firestoreService.updateNote(docID, text); 326 | } 327 | // Reset the form 328 | textController.clear(); 329 | } 330 | }, 331 | child: Text(docID == null ? 'Add' : 'Update'), 332 | ), 333 | ], 334 | ), 335 | ).then((_) { 336 | // Reset if user taps outside dialog 337 | textController.clear(); 338 | }); 339 | } 340 | ``` 341 | 342 | Here’s what it does: 343 | 344 | - Shows a dialog with a text field. 345 | - If it’s an **update**, it fills in the old note text. 346 | - When submitted, it either calls `addNote()` or `updateNote()` depending on whether a `docID` was passed. 347 | 348 |

349 | 350 | 351 |

352 | 353 | --- 354 | 355 | #### 📝 Displaying Notes 356 | 357 |

358 | 359 |

360 | 361 | We use a `StreamBuilder` to **automatically update the list of notes**: 362 | 363 | ```dart 364 | body: StreamBuilder( 365 | stream: firestoreService.getNotesStream(), 366 | builder: (context, snapshot) { 367 | if (snapshot.hasData) { 368 | List notesList = snapshot.data!.docs; 369 | ``` 370 | 371 | - `getNotesStream()` gives us live updates from Firestore. 372 | - Every time a note is added, updated, or deleted, the UI will refresh. 373 | 374 | Inside the list builder: 375 | 376 | ```dart 377 | return ListView.builder( 378 | itemCount: notesList.length, 379 | itemBuilder: (context, index) { 380 | DocumentSnapshot document = notesList[index]; 381 | String docID = document.id; 382 | Map data = document.data() as Map; 383 | String noteText = data['note']; 384 | ``` 385 | 386 | We extract the note text and document ID so we can display it and know which one to update or delete. Each note looks like this: 387 | 388 |

389 | 390 |

391 | 392 | ```dart 393 | return ListTile( 394 | title: Text(noteText), 395 | trailing: Row( 396 | mainAxisSize: MainAxisSize.min, 397 | children: [ 398 | IconButton( 399 | icon: Icon(Icons.settings), 400 | onPressed: () => openNoteBox(docID: docID, existingText: noteText), 401 | ), 402 | IconButton( 403 | icon: Icon(Icons.delete), 404 | onPressed: () => firestoreService.deleteNote(docID), 405 | ), 406 | ], 407 | ), 408 | ); 409 | ``` 410 | 411 | It simply consists of: 412 | - The note's text. 413 | - One button to edit. 414 | - One button to delete. 415 | 416 | --- 417 | 418 | ### 🔧 `firestore.dart`: Firebase Service Code 419 | 420 | This file handles communication to Firestore, so we don’t clutter the UI code. Here's how your database should look like in the Firestore menu inside Firebase Console. 421 | 422 |

423 | 424 |

425 | 426 | The Firestore Service contains 4 functionalities: 427 | - Add (Create) 428 | - Get (Read) 429 | - Update 430 | - Delete 431 | 432 | --- 433 | 434 | #### 📌 Add a Note 435 | 436 | ```dart 437 | Future addNote(String note) { 438 | return notes.add({ 439 | 'note': note, 440 | 'timestamp': Timestamp.now(), 441 | }); 442 | } 443 | ``` 444 | 445 | - Adds a new note to the `notes` collection. 446 | - Also stores the current timestamp so we can sort later. 447 | 448 | --- 449 | 450 | #### 🔁 Get Notes as a Stream 451 | 452 | ```dart 453 | Stream getNotesStream() { 454 | return notes.orderBy('timestamp', descending: true).snapshots(); 455 | } 456 | ``` 457 | 458 | - This gives us real-time updates whenever the data changes. 459 | 460 | --- 461 | 462 | #### ✏️ Update a Note 463 | 464 | ```dart 465 | Future updateNote(String docID, String newNote) { 466 | return notes.doc(docID).update({ 467 | 'note': newNote, 468 | 'timestamp': Timestamp.now(), 469 | }); 470 | } 471 | ``` 472 | 473 | - Updates the text and updates the timestamp (so the note moves to the top). 474 | 475 | --- 476 | 477 | #### 🗑 Delete a Note 478 | 479 | ```dart 480 | Future deleteNote(String docID) { 481 | return notes.doc(docID).delete(); 482 | } 483 | ``` 484 | 485 | - Deletes the note with the given document ID. 486 | -------------------------------------------------------------------------------- /04. Stateful and Starting the World Time App/README.md: -------------------------------------------------------------------------------- 1 | # 04. Stateful and Starting the World Time App 2 | 3 | [Previous](/03.%20Widgets%20-%20Images,%20Buttons,%20Icons,%20Containers%20&%20Padding,%20Rows,%20Columns/) | [Main Page](/) | [Next](/05.%20Firebase/) 4 | 5 | ## Content Outline 6 | 7 | - [State](#state) 8 | - [Key Concepts of State in Flutter](#key-concepts-of-state-in-flutter) 9 | - [Stateless vs. Stateful Widgets](#stateless-vs-stateful-widgets) 10 | - [How State Works](#how-state-works) 11 | - [Types of State](#types-of-state) 12 | - [Example: Counter App with StatefulWidget](#example-counter-app-with-statefulwidget) 13 | - [Why Is State Important?](#why-is-state-important) 14 | - [Stateless Widgets](#stateless-widgets) 15 | - [Example of a Stateless Widget](#example-of-a-stateless-widget) 16 | - [Stateful Widgets](#stateful-widgets) 17 | - [Example of a Stateful Widget](#example-of-a-stateful-widget) 18 | - [Comparison: Stateless vs. Stateful Widgets](#comparison-stateless-vs-stateful-widgets) 19 | - [Example of Stateless vs. Stateful](#example-of-stateless-vs-stateful) 20 | - [Stateless Widget Example](#stateless-widget-example) 21 | - [Stateful Widget Example](#stateful-widget-example) 22 | - [Comparison: Stateless App vs. Stateful App](#comparison-stateless-app-vs-stateful-app) 23 | 24 | ## State 25 | 26 | In Flutter, *state* refers to the data or information that a widget uses to determine its appearance and behavior. Simply put, state is what makes an app dynamic and interactive by allowing the user interface (UI) to change in response to user actions or other events. 27 | 28 | ### Key Concepts of State in Flutter 29 | 30 | 1. **Stateless vs. Stateful Widgets** 31 | - **Stateless Widgets**: These widgets do not have any state. They are immutable, meaning their appearance and behavior cannot change after being built. Use them for static UI elements like text or icons. 32 | - Example: A `Text` widget displaying "Hello, World!" is stateless because it doesn’t change. 33 | - **Stateful Widgets**: These widgets can change their appearance and behavior during their lifecycle. They are used when the UI needs to update dynamically based on user interaction or other events. 34 | - Example: A counter app where a button increments a number is stateful. 35 | 36 | 2. **How State Works** 37 | - In Flutter, apps are reactive. This means when the state changes, Flutter automatically rebuilds the affected parts of the UI to reflect those changes. 38 | - For example, if a button changes color when clicked, the state is updated, and Flutter redraws the button with the new color. 39 | 40 | 3. **Types of State** 41 | - **Ephemeral State**: This is local to a single widget and doesn’t need to be shared across multiple widgets. It can be managed using `StatefulWidget` and `setState()`. 42 | - Example: The currently selected tab in a bottom navigation bar. 43 | - **App State**: This is shared across multiple parts of an app and persists beyond a single widget’s lifecycle. It often requires more advanced state management techniques like `Provider`, `BLoC`, or `Riverpod`. 44 | - Example: User login status or shopping cart data in an e-commerce app. 45 | 46 | --- 47 | 48 | ### Example: Counter App with StatefulWidget 49 | 50 |
51 | 52 |
53 | 54 | ```dart 55 | import 'package:flutter/material.dart'; 56 | 57 | void main() => runApp(MyApp()); 58 | 59 | class MyApp extends StatelessWidget { 60 | @override 61 | Widget build(BuildContext context) { 62 | return MaterialApp( 63 | home: CounterApp(), 64 | ); 65 | } 66 | } 67 | 68 | class CounterApp extends StatefulWidget { 69 | @override 70 | _CounterAppState createState() => _CounterAppState(); 71 | } 72 | 73 | class _CounterAppState extends State { 74 | int _counter = 0; // This is the state (data) that changes. 75 | 76 | void _incrementCounter() { 77 | setState(() { 78 | _counter++; // Update the state. 79 | }); 80 | } 81 | 82 | @override 83 | Widget build(BuildContext context) { 84 | return Scaffold( 85 | appBar: AppBar(title: Text('Counter App')), 86 | body: Center( 87 | child: Column( 88 | mainAxisAlignment: MainAxisAlignment.center, 89 | children: [ 90 | Text('You have pressed the button this many times:'), 91 | Text('$_counter', style: TextStyle(fontSize: 32)), 92 | ], 93 | ), 94 | ), 95 | floatingActionButton: FloatingActionButton( 96 | onPressed: _incrementCounter, 97 | child: Icon(Icons.add), 98 | ), 99 | ); 100 | } 101 | } 102 | ``` 103 | 104 | **Explanation**: 105 | - `_counter`: This variable holds the current count, which is part of the widget's state. 106 | - `setState()`: This method tells Flutter that the state has changed, prompting it to rebuild the UI with the updated value. 107 | 108 | --- 109 | 110 | ### Why Is State Important? 111 | Without state, apps would be static and unable to respond to user interactions. For example: 112 | - A login form needs to update its UI based on whether the user input is valid. 113 | - A music player needs to show whether a song is playing or paused. 114 | 115 | --- 116 | 117 | ## **Stateless Widgets** 118 | Stateless widgets are widgets that do not change during their lifetime. They are immutable, meaning once created, their properties cannot be updated. These widgets are ideal for displaying *static content* that does not depend on user interactions or dynamic data. 119 | 120 | ### Example of a Stateless Widget: 121 | 122 |
123 | 124 |
125 | 126 | ```dart 127 | import 'package:flutter/material.dart'; 128 | 129 | class MyStatelessWidget extends StatelessWidget { 130 | @override 131 | Widget build(BuildContext context) { 132 | return Scaffold( 133 | appBar: AppBar(title: Text('Stateless Widget Example')), 134 | body: Center( 135 | child: Text('Hello, World!', style: TextStyle(fontSize: 24)), 136 | ), 137 | ); 138 | } 139 | } 140 | 141 | void main() { 142 | runApp(MaterialApp( 143 | home: MyStatelessWidget(), 144 | )); 145 | } 146 | ``` 147 | **Explanation**: 148 | - The text "Hello, World!" is static and will not change regardless of user actions. 149 | 150 | --- 151 | 152 | ## **Stateful Widgets** 153 | Stateful widgets are widgets that can change their state during their lifecycle. They are mutable and allow the UI to update dynamically in response to user interactions, animations, or external events. 154 | 155 | ### Example of a Stateful Widget: 156 | 157 |
158 | 159 |
160 | 161 | ```dart 162 | import 'package:flutter/material.dart'; 163 | 164 | class MyStatefulWidget extends StatefulWidget { 165 | @override 166 | _MyStatefulWidgetState createState() => _MyStatefulWidgetState(); 167 | } 168 | 169 | class _MyStatefulWidgetState extends State { 170 | int _counter = 0; 171 | 172 | void _incrementCounter() { 173 | setState(() { 174 | _counter++; // Updates the state 175 | }); 176 | } 177 | 178 | @override 179 | Widget build(BuildContext context) { 180 | return Scaffold( 181 | appBar: AppBar(title: Text('Stateful Widget Example')), 182 | body: Center( 183 | child: Column( 184 | mainAxisAlignment: MainAxisAlignment.center, 185 | children: [ 186 | Text('Counter:', style: TextStyle(fontSize: 24)), 187 | Text('$_counter', style: TextStyle(fontSize: 48)), 188 | ], 189 | ), 190 | ), 191 | floatingActionButton: FloatingActionButton( 192 | onPressed: _incrementCounter, 193 | child: Icon(Icons.add), 194 | ), 195 | ); 196 | } 197 | } 198 | 199 | void main() => runApp(MaterialApp(home: MyStatefulWidget())); 200 | ``` 201 | **Explanation**: 202 | - The `_counter` variable is part of the widget's state. 203 | - Clicking the button updates `_counter`, and the UI reflects this change. 204 | 205 | --- 206 | 207 | ## **Comparison: Stateless vs. Stateful Widgets** 208 | 209 | | Feature | Stateless Widgets | Stateful Widgets | 210 | |--------------------------|--------------------------------------------|-------------------------------------------| 211 | | **Mutability** | Immutable (cannot change after creation). | Mutable (can change during lifecycle). | 212 | | **Use Case** | Static UI elements (e.g., text, icons). | Dynamic UI elements (e.g., forms, counters). | 213 | | **Performance** | Lightweight and efficient. | Slightly heavier due to state management. | 214 | | **Examples** | Displaying fixed text or images. | Buttons that update counters or animations. | 215 | | **Lifecycle Management** | No state management required. | Requires `setState()` to manage changes. | 216 | 217 | --- 218 | 219 | ## Example of Stateless vs Stateful Widgets 220 | Here are two examples of a simple app: one using a **Stateless Widget** and the same app as a **Stateful Widget**. Both apps display a button and some text. The difference is how they handle *state* (whether the text changes when the button is pressed). 221 | 222 | --- 223 | 224 | ### **Stateless Widget Example** 225 | 226 |
227 | 228 |
229 | 230 | This app displays a button and some text. The text does *not* change when the button is pressed because it’s static. 231 | 232 | ```dart 233 | import 'package:flutter/material.dart'; 234 | 235 | class StatelessApp extends StatelessWidget { 236 | @override 237 | Widget build(BuildContext context) { 238 | return MaterialApp( 239 | home: Scaffold( 240 | appBar: AppBar(title: Text('Stateless App')), 241 | body: Center( 242 | child: Column( 243 | mainAxisAlignment: MainAxisAlignment.center, 244 | children: [ 245 | Text('This is a Stateless Widget', style: TextStyle(fontSize: 20)), 246 | SizedBox(height: 20), // Adds some space between widgets 247 | ElevatedButton( 248 | onPressed: () { 249 | // Button does nothing because state cannot change 250 | print('Button Pressed!'); 251 | }, 252 | child: Text('Press Me'), 253 | ), 254 | ], 255 | ), 256 | ), 257 | ), 258 | ); 259 | } 260 | } 261 | 262 | void main() => runApp(StatelessApp()); 263 | ``` 264 | 265 | **Explanation**: 266 | - The text "This is a Stateless Widget" is fixed and does not change, even if the button is pressed. 267 | - The `onPressed` function of the button only prints a message to the console but does not affect the UI. 268 | 269 | --- 270 | 271 | ### **Stateful Widget Example** 272 | 273 |
274 | 275 |
276 | 277 | This app displays a button and some text. When the button is pressed, the text changes dynamically because it uses *state*. 278 | 279 | ```dart 280 | import 'package:flutter/material.dart'; 281 | 282 | class StatefulApp extends StatefulWidget { 283 | @override 284 | _StatefulAppState createState() => _StatefulAppState(); 285 | } 286 | 287 | class _StatefulAppState extends State { 288 | String _message = 'This is a Stateful Widget'; // Initial state 289 | 290 | void _changeMessage() { 291 | setState(() { 292 | _message = 'The Button was Pressed!'; // Update the state 293 | }); 294 | } 295 | 296 | @override 297 | Widget build(BuildContext context) { 298 | return MaterialApp( 299 | home: Scaffold( 300 | appBar: AppBar(title: Text('Stateful App')), 301 | body: Center( 302 | child: Column( 303 | mainAxisAlignment: MainAxisAlignment.center, 304 | children: [ 305 | Text(_message, style: TextStyle(fontSize: 20)), 306 | SizedBox(height: 20), 307 | ElevatedButton( 308 | onPressed: _changeMessage, 309 | child: Text('Press Me'), 310 | ), 311 | ], 312 | ), 313 | ), 314 | ), 315 | ); 316 | } 317 | } 318 | 319 | void main() => runApp(StatefulApp()); 320 | ``` 321 | 322 | **Explanation**: 323 | - The `_message` variable holds the current state of the text. 324 | - When the button is pressed, `_changeMessage()` updates `_message` using `setState()`, and Flutter rebuilds the UI to show the new message. 325 | 326 | --- 327 | 328 | ### **Comparison: Stateless App vs. Stateful App** 329 | 330 | | Feature | Stateless App | Stateful App | 331 | |-------------------------|--------------------------------------------|-------------------------------------------| 332 | | **Behavior** | The text remains static, even if you press the button. | The text changes dynamically when you press the button. | 333 | | **Code Simplicity** | Simpler because there’s no state to manage. | Slightly more complex because it manages state with `setState()`. | 334 | | **Interactivity** | No interactivity beyond pressing the button. | Interactive because pressing the button updates the UI. | 335 | 336 | --- 337 | 338 | ### A More Complex Example 339 | 340 |
341 | 342 |
343 | 344 | ```dart 345 | import 'package:flutter/material.dart'; 346 | 347 | void main() { 348 | runApp(const MyApp()); 349 | } 350 | 351 | class MyApp extends StatelessWidget { 352 | const MyApp({super.key}); 353 | 354 | // This widget is the root of your application. 355 | @override 356 | Widget build(BuildContext context) { 357 | return MaterialApp( 358 | title: 'Flutter Demo', 359 | theme: ThemeData( 360 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), 361 | useMaterial3: true, 362 | ), 363 | home: const RowColumnPage(), 364 | ); 365 | } 366 | } 367 | 368 | class RowColumnPage extends StatelessWidget { 369 | const RowColumnPage({Key? key}) : super(key: key); 370 | 371 | @override 372 | Widget build(BuildContext context) { 373 | MediaQueryData mediaQueryData = MediaQuery.of(context); 374 | double screenWidth = mediaQueryData.size.width; 375 | double screenHeight = mediaQueryData.size.height; 376 | return Scaffold( 377 | appBar: AppBar( 378 | title: const Text( 379 | 'My First App', 380 | style: TextStyle(color: Colors.black), 381 | ), 382 | backgroundColor: Colors.orange[200], 383 | centerTitle: true, 384 | ), 385 | body: Column( 386 | crossAxisAlignment: CrossAxisAlignment.center, 387 | mainAxisAlignment: MainAxisAlignment.center, 388 | children: [ 389 | Container( 390 | child: AspectRatio( 391 | aspectRatio: 1.0, 392 | child: Container( 393 | width: MediaQuery.of(context).size.width, 394 | margin: EdgeInsets.fromLTRB(20.0, 5.0, 20.0, 10.0), 395 | padding: EdgeInsets.all(20.0), 396 | color: Colors.lightBlue[100], 397 | child: Center( 398 | child: Image.network( 399 | 'https://picsum.photos/200', 400 | fit: BoxFit.cover, 401 | width: 500, 402 | ), 403 | ), 404 | ), 405 | ), 406 | ), 407 | Container( 408 | width: MediaQuery.of(context).size.width, 409 | margin: EdgeInsets.fromLTRB(20.0, 5.0, 20.0, 10.0), 410 | padding: EdgeInsets.all(20.0), 411 | color: Colors.pink[200], 412 | child: Text('What image is that', style: TextStyle(fontSize: 16)), 413 | ), 414 | Container( 415 | width: MediaQuery.of(context).size.width, 416 | color: Colors.yellow[200], 417 | padding: EdgeInsets.all(20.0), 418 | margin: EdgeInsets.fromLTRB(20.0, 5.0, 20.0, 5.0), 419 | child: Row( 420 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 421 | crossAxisAlignment: CrossAxisAlignment.start, 422 | children: [ 423 | Column(children: [Icon(Icons.food_bank), Text("Food")]), 424 | Column(children: [Icon(Icons.landscape), Text("Scenery")]), 425 | Column(children: [Icon(Icons.people), Text("People")]), 426 | ], 427 | ), 428 | ), 429 | CounterCard(), 430 | ], 431 | ), 432 | ); 433 | } 434 | } 435 | 436 | class CounterCard extends StatefulWidget { 437 | const CounterCard({super.key}); 438 | 439 | @override 440 | State createState() => _CounterCardState(); 441 | } 442 | 443 | class _CounterCardState extends State { 444 | int _counter = 0; // This is the state (data) that changes. 445 | 446 | void _incrementCounter() { 447 | setState(() { 448 | _counter++; // Update the state. 449 | }); 450 | } 451 | 452 | @override 453 | Widget build(BuildContext context) { 454 | return Container( 455 | margin: EdgeInsets.fromLTRB(20.0, 5.0, 20.0, 5.0), 456 | padding: EdgeInsets.all(20.0), 457 | width: MediaQuery.of(context).size.width, 458 | color: Colors.cyan[100], 459 | child: Row( 460 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 461 | children: [ 462 | Text("Counter here: $_counter", style: TextStyle(fontSize: 16)), 463 | Container( 464 | color: Colors.cyan[200], 465 | padding: EdgeInsets.all(5.0), 466 | child: IconButton( 467 | onPressed: _incrementCounter, 468 | icon: Icon(Icons.add, color: Colors.black, size: 16), 469 | ), 470 | ), 471 | ], 472 | ), 473 | ); 474 | } 475 | } 476 | ``` 477 | -------------------------------------------------------------------------------- /06. CRUD Local Database/crud_local_database_app/lib/models/note.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'note.dart'; 4 | 5 | // ************************************************************************** 6 | // IsarCollectionGenerator 7 | // ************************************************************************** 8 | 9 | // coverage:ignore-file 10 | // ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types 11 | 12 | extension GetNoteCollection on Isar { 13 | IsarCollection get notes => this.collection(); 14 | } 15 | 16 | const NoteSchema = CollectionSchema( 17 | name: r'Note', 18 | id: 6284318083599466921, 19 | properties: { 20 | r'imagePath': PropertySchema( 21 | id: 0, 22 | name: r'imagePath', 23 | type: IsarType.string, 24 | ), 25 | r'text': PropertySchema( 26 | id: 1, 27 | name: r'text', 28 | type: IsarType.string, 29 | ) 30 | }, 31 | estimateSize: _noteEstimateSize, 32 | serialize: _noteSerialize, 33 | deserialize: _noteDeserialize, 34 | deserializeProp: _noteDeserializeProp, 35 | idName: r'id', 36 | indexes: {}, 37 | links: {}, 38 | embeddedSchemas: {}, 39 | getId: _noteGetId, 40 | getLinks: _noteGetLinks, 41 | attach: _noteAttach, 42 | version: '3.1.0+1', 43 | ); 44 | 45 | int _noteEstimateSize( 46 | Note object, 47 | List offsets, 48 | Map> allOffsets, 49 | ) { 50 | var bytesCount = offsets.last; 51 | bytesCount += 3 + object.imagePath.length * 3; 52 | bytesCount += 3 + object.text.length * 3; 53 | return bytesCount; 54 | } 55 | 56 | void _noteSerialize( 57 | Note object, 58 | IsarWriter writer, 59 | List offsets, 60 | Map> allOffsets, 61 | ) { 62 | writer.writeString(offsets[0], object.imagePath); 63 | writer.writeString(offsets[1], object.text); 64 | } 65 | 66 | Note _noteDeserialize( 67 | Id id, 68 | IsarReader reader, 69 | List offsets, 70 | Map> allOffsets, 71 | ) { 72 | final object = Note(); 73 | object.id = id; 74 | object.imagePath = reader.readString(offsets[0]); 75 | object.text = reader.readString(offsets[1]); 76 | return object; 77 | } 78 | 79 | P _noteDeserializeProp

( 80 | IsarReader reader, 81 | int propertyId, 82 | int offset, 83 | Map> allOffsets, 84 | ) { 85 | switch (propertyId) { 86 | case 0: 87 | return (reader.readString(offset)) as P; 88 | case 1: 89 | return (reader.readString(offset)) as P; 90 | default: 91 | throw IsarError('Unknown property with id $propertyId'); 92 | } 93 | } 94 | 95 | Id _noteGetId(Note object) { 96 | return object.id; 97 | } 98 | 99 | List> _noteGetLinks(Note object) { 100 | return []; 101 | } 102 | 103 | void _noteAttach(IsarCollection col, Id id, Note object) { 104 | object.id = id; 105 | } 106 | 107 | extension NoteQueryWhereSort on QueryBuilder { 108 | QueryBuilder anyId() { 109 | return QueryBuilder.apply(this, (query) { 110 | return query.addWhereClause(const IdWhereClause.any()); 111 | }); 112 | } 113 | } 114 | 115 | extension NoteQueryWhere on QueryBuilder { 116 | QueryBuilder idEqualTo(Id id) { 117 | return QueryBuilder.apply(this, (query) { 118 | return query.addWhereClause(IdWhereClause.between( 119 | lower: id, 120 | upper: id, 121 | )); 122 | }); 123 | } 124 | 125 | QueryBuilder idNotEqualTo(Id id) { 126 | return QueryBuilder.apply(this, (query) { 127 | if (query.whereSort == Sort.asc) { 128 | return query 129 | .addWhereClause( 130 | IdWhereClause.lessThan(upper: id, includeUpper: false), 131 | ) 132 | .addWhereClause( 133 | IdWhereClause.greaterThan(lower: id, includeLower: false), 134 | ); 135 | } else { 136 | return query 137 | .addWhereClause( 138 | IdWhereClause.greaterThan(lower: id, includeLower: false), 139 | ) 140 | .addWhereClause( 141 | IdWhereClause.lessThan(upper: id, includeUpper: false), 142 | ); 143 | } 144 | }); 145 | } 146 | 147 | QueryBuilder idGreaterThan(Id id, 148 | {bool include = false}) { 149 | return QueryBuilder.apply(this, (query) { 150 | return query.addWhereClause( 151 | IdWhereClause.greaterThan(lower: id, includeLower: include), 152 | ); 153 | }); 154 | } 155 | 156 | QueryBuilder idLessThan(Id id, 157 | {bool include = false}) { 158 | return QueryBuilder.apply(this, (query) { 159 | return query.addWhereClause( 160 | IdWhereClause.lessThan(upper: id, includeUpper: include), 161 | ); 162 | }); 163 | } 164 | 165 | QueryBuilder idBetween( 166 | Id lowerId, 167 | Id upperId, { 168 | bool includeLower = true, 169 | bool includeUpper = true, 170 | }) { 171 | return QueryBuilder.apply(this, (query) { 172 | return query.addWhereClause(IdWhereClause.between( 173 | lower: lowerId, 174 | includeLower: includeLower, 175 | upper: upperId, 176 | includeUpper: includeUpper, 177 | )); 178 | }); 179 | } 180 | } 181 | 182 | extension NoteQueryFilter on QueryBuilder { 183 | QueryBuilder idEqualTo(Id value) { 184 | return QueryBuilder.apply(this, (query) { 185 | return query.addFilterCondition(FilterCondition.equalTo( 186 | property: r'id', 187 | value: value, 188 | )); 189 | }); 190 | } 191 | 192 | QueryBuilder idGreaterThan( 193 | Id value, { 194 | bool include = false, 195 | }) { 196 | return QueryBuilder.apply(this, (query) { 197 | return query.addFilterCondition(FilterCondition.greaterThan( 198 | include: include, 199 | property: r'id', 200 | value: value, 201 | )); 202 | }); 203 | } 204 | 205 | QueryBuilder idLessThan( 206 | Id value, { 207 | bool include = false, 208 | }) { 209 | return QueryBuilder.apply(this, (query) { 210 | return query.addFilterCondition(FilterCondition.lessThan( 211 | include: include, 212 | property: r'id', 213 | value: value, 214 | )); 215 | }); 216 | } 217 | 218 | QueryBuilder idBetween( 219 | Id lower, 220 | Id upper, { 221 | bool includeLower = true, 222 | bool includeUpper = true, 223 | }) { 224 | return QueryBuilder.apply(this, (query) { 225 | return query.addFilterCondition(FilterCondition.between( 226 | property: r'id', 227 | lower: lower, 228 | includeLower: includeLower, 229 | upper: upper, 230 | includeUpper: includeUpper, 231 | )); 232 | }); 233 | } 234 | 235 | QueryBuilder imagePathEqualTo( 236 | String value, { 237 | bool caseSensitive = true, 238 | }) { 239 | return QueryBuilder.apply(this, (query) { 240 | return query.addFilterCondition(FilterCondition.equalTo( 241 | property: r'imagePath', 242 | value: value, 243 | caseSensitive: caseSensitive, 244 | )); 245 | }); 246 | } 247 | 248 | QueryBuilder imagePathGreaterThan( 249 | String value, { 250 | bool include = false, 251 | bool caseSensitive = true, 252 | }) { 253 | return QueryBuilder.apply(this, (query) { 254 | return query.addFilterCondition(FilterCondition.greaterThan( 255 | include: include, 256 | property: r'imagePath', 257 | value: value, 258 | caseSensitive: caseSensitive, 259 | )); 260 | }); 261 | } 262 | 263 | QueryBuilder imagePathLessThan( 264 | String value, { 265 | bool include = false, 266 | bool caseSensitive = true, 267 | }) { 268 | return QueryBuilder.apply(this, (query) { 269 | return query.addFilterCondition(FilterCondition.lessThan( 270 | include: include, 271 | property: r'imagePath', 272 | value: value, 273 | caseSensitive: caseSensitive, 274 | )); 275 | }); 276 | } 277 | 278 | QueryBuilder imagePathBetween( 279 | String lower, 280 | String upper, { 281 | bool includeLower = true, 282 | bool includeUpper = true, 283 | bool caseSensitive = true, 284 | }) { 285 | return QueryBuilder.apply(this, (query) { 286 | return query.addFilterCondition(FilterCondition.between( 287 | property: r'imagePath', 288 | lower: lower, 289 | includeLower: includeLower, 290 | upper: upper, 291 | includeUpper: includeUpper, 292 | caseSensitive: caseSensitive, 293 | )); 294 | }); 295 | } 296 | 297 | QueryBuilder imagePathStartsWith( 298 | String value, { 299 | bool caseSensitive = true, 300 | }) { 301 | return QueryBuilder.apply(this, (query) { 302 | return query.addFilterCondition(FilterCondition.startsWith( 303 | property: r'imagePath', 304 | value: value, 305 | caseSensitive: caseSensitive, 306 | )); 307 | }); 308 | } 309 | 310 | QueryBuilder imagePathEndsWith( 311 | String value, { 312 | bool caseSensitive = true, 313 | }) { 314 | return QueryBuilder.apply(this, (query) { 315 | return query.addFilterCondition(FilterCondition.endsWith( 316 | property: r'imagePath', 317 | value: value, 318 | caseSensitive: caseSensitive, 319 | )); 320 | }); 321 | } 322 | 323 | QueryBuilder imagePathContains( 324 | String value, 325 | {bool caseSensitive = true}) { 326 | return QueryBuilder.apply(this, (query) { 327 | return query.addFilterCondition(FilterCondition.contains( 328 | property: r'imagePath', 329 | value: value, 330 | caseSensitive: caseSensitive, 331 | )); 332 | }); 333 | } 334 | 335 | QueryBuilder imagePathMatches( 336 | String pattern, 337 | {bool caseSensitive = true}) { 338 | return QueryBuilder.apply(this, (query) { 339 | return query.addFilterCondition(FilterCondition.matches( 340 | property: r'imagePath', 341 | wildcard: pattern, 342 | caseSensitive: caseSensitive, 343 | )); 344 | }); 345 | } 346 | 347 | QueryBuilder imagePathIsEmpty() { 348 | return QueryBuilder.apply(this, (query) { 349 | return query.addFilterCondition(FilterCondition.equalTo( 350 | property: r'imagePath', 351 | value: '', 352 | )); 353 | }); 354 | } 355 | 356 | QueryBuilder imagePathIsNotEmpty() { 357 | return QueryBuilder.apply(this, (query) { 358 | return query.addFilterCondition(FilterCondition.greaterThan( 359 | property: r'imagePath', 360 | value: '', 361 | )); 362 | }); 363 | } 364 | 365 | QueryBuilder textEqualTo( 366 | String value, { 367 | bool caseSensitive = true, 368 | }) { 369 | return QueryBuilder.apply(this, (query) { 370 | return query.addFilterCondition(FilterCondition.equalTo( 371 | property: r'text', 372 | value: value, 373 | caseSensitive: caseSensitive, 374 | )); 375 | }); 376 | } 377 | 378 | QueryBuilder textGreaterThan( 379 | String value, { 380 | bool include = false, 381 | bool caseSensitive = true, 382 | }) { 383 | return QueryBuilder.apply(this, (query) { 384 | return query.addFilterCondition(FilterCondition.greaterThan( 385 | include: include, 386 | property: r'text', 387 | value: value, 388 | caseSensitive: caseSensitive, 389 | )); 390 | }); 391 | } 392 | 393 | QueryBuilder textLessThan( 394 | String value, { 395 | bool include = false, 396 | bool caseSensitive = true, 397 | }) { 398 | return QueryBuilder.apply(this, (query) { 399 | return query.addFilterCondition(FilterCondition.lessThan( 400 | include: include, 401 | property: r'text', 402 | value: value, 403 | caseSensitive: caseSensitive, 404 | )); 405 | }); 406 | } 407 | 408 | QueryBuilder textBetween( 409 | String lower, 410 | String upper, { 411 | bool includeLower = true, 412 | bool includeUpper = true, 413 | bool caseSensitive = true, 414 | }) { 415 | return QueryBuilder.apply(this, (query) { 416 | return query.addFilterCondition(FilterCondition.between( 417 | property: r'text', 418 | lower: lower, 419 | includeLower: includeLower, 420 | upper: upper, 421 | includeUpper: includeUpper, 422 | caseSensitive: caseSensitive, 423 | )); 424 | }); 425 | } 426 | 427 | QueryBuilder textStartsWith( 428 | String value, { 429 | bool caseSensitive = true, 430 | }) { 431 | return QueryBuilder.apply(this, (query) { 432 | return query.addFilterCondition(FilterCondition.startsWith( 433 | property: r'text', 434 | value: value, 435 | caseSensitive: caseSensitive, 436 | )); 437 | }); 438 | } 439 | 440 | QueryBuilder textEndsWith( 441 | String value, { 442 | bool caseSensitive = true, 443 | }) { 444 | return QueryBuilder.apply(this, (query) { 445 | return query.addFilterCondition(FilterCondition.endsWith( 446 | property: r'text', 447 | value: value, 448 | caseSensitive: caseSensitive, 449 | )); 450 | }); 451 | } 452 | 453 | QueryBuilder textContains(String value, 454 | {bool caseSensitive = true}) { 455 | return QueryBuilder.apply(this, (query) { 456 | return query.addFilterCondition(FilterCondition.contains( 457 | property: r'text', 458 | value: value, 459 | caseSensitive: caseSensitive, 460 | )); 461 | }); 462 | } 463 | 464 | QueryBuilder textMatches(String pattern, 465 | {bool caseSensitive = true}) { 466 | return QueryBuilder.apply(this, (query) { 467 | return query.addFilterCondition(FilterCondition.matches( 468 | property: r'text', 469 | wildcard: pattern, 470 | caseSensitive: caseSensitive, 471 | )); 472 | }); 473 | } 474 | 475 | QueryBuilder textIsEmpty() { 476 | return QueryBuilder.apply(this, (query) { 477 | return query.addFilterCondition(FilterCondition.equalTo( 478 | property: r'text', 479 | value: '', 480 | )); 481 | }); 482 | } 483 | 484 | QueryBuilder textIsNotEmpty() { 485 | return QueryBuilder.apply(this, (query) { 486 | return query.addFilterCondition(FilterCondition.greaterThan( 487 | property: r'text', 488 | value: '', 489 | )); 490 | }); 491 | } 492 | } 493 | 494 | extension NoteQueryObject on QueryBuilder {} 495 | 496 | extension NoteQueryLinks on QueryBuilder {} 497 | 498 | extension NoteQuerySortBy on QueryBuilder { 499 | QueryBuilder sortByImagePath() { 500 | return QueryBuilder.apply(this, (query) { 501 | return query.addSortBy(r'imagePath', Sort.asc); 502 | }); 503 | } 504 | 505 | QueryBuilder sortByImagePathDesc() { 506 | return QueryBuilder.apply(this, (query) { 507 | return query.addSortBy(r'imagePath', Sort.desc); 508 | }); 509 | } 510 | 511 | QueryBuilder sortByText() { 512 | return QueryBuilder.apply(this, (query) { 513 | return query.addSortBy(r'text', Sort.asc); 514 | }); 515 | } 516 | 517 | QueryBuilder sortByTextDesc() { 518 | return QueryBuilder.apply(this, (query) { 519 | return query.addSortBy(r'text', Sort.desc); 520 | }); 521 | } 522 | } 523 | 524 | extension NoteQuerySortThenBy on QueryBuilder { 525 | QueryBuilder thenById() { 526 | return QueryBuilder.apply(this, (query) { 527 | return query.addSortBy(r'id', Sort.asc); 528 | }); 529 | } 530 | 531 | QueryBuilder thenByIdDesc() { 532 | return QueryBuilder.apply(this, (query) { 533 | return query.addSortBy(r'id', Sort.desc); 534 | }); 535 | } 536 | 537 | QueryBuilder thenByImagePath() { 538 | return QueryBuilder.apply(this, (query) { 539 | return query.addSortBy(r'imagePath', Sort.asc); 540 | }); 541 | } 542 | 543 | QueryBuilder thenByImagePathDesc() { 544 | return QueryBuilder.apply(this, (query) { 545 | return query.addSortBy(r'imagePath', Sort.desc); 546 | }); 547 | } 548 | 549 | QueryBuilder thenByText() { 550 | return QueryBuilder.apply(this, (query) { 551 | return query.addSortBy(r'text', Sort.asc); 552 | }); 553 | } 554 | 555 | QueryBuilder thenByTextDesc() { 556 | return QueryBuilder.apply(this, (query) { 557 | return query.addSortBy(r'text', Sort.desc); 558 | }); 559 | } 560 | } 561 | 562 | extension NoteQueryWhereDistinct on QueryBuilder { 563 | QueryBuilder distinctByImagePath( 564 | {bool caseSensitive = true}) { 565 | return QueryBuilder.apply(this, (query) { 566 | return query.addDistinctBy(r'imagePath', caseSensitive: caseSensitive); 567 | }); 568 | } 569 | 570 | QueryBuilder distinctByText( 571 | {bool caseSensitive = true}) { 572 | return QueryBuilder.apply(this, (query) { 573 | return query.addDistinctBy(r'text', caseSensitive: caseSensitive); 574 | }); 575 | } 576 | } 577 | 578 | extension NoteQueryProperty on QueryBuilder { 579 | QueryBuilder idProperty() { 580 | return QueryBuilder.apply(this, (query) { 581 | return query.addPropertyName(r'id'); 582 | }); 583 | } 584 | 585 | QueryBuilder imagePathProperty() { 586 | return QueryBuilder.apply(this, (query) { 587 | return query.addPropertyName(r'imagePath'); 588 | }); 589 | } 590 | 591 | QueryBuilder textProperty() { 592 | return QueryBuilder.apply(this, (query) { 593 | return query.addPropertyName(r'text'); 594 | }); 595 | } 596 | } 597 | -------------------------------------------------------------------------------- /06. CRUD Local Database/README.md: -------------------------------------------------------------------------------- 1 | # 06. CRUD Local Database 2 | 3 | [Previous](/05.%20Form/) | [Main Page](/) | [Next](/07.%20Relational%20Table/) 4 | 5 | ## Content Outline 6 | 7 | - [Setup](#setup) 8 | - [Note](#note) 9 | - [Note Database](#note-database) 10 | - [Initialization](#initialization) 11 | - [Create](#create) 12 | - [Read](#read) 13 | - [Update](#update) 14 | - [Delete](#delete) 15 | - [CRUD Page](#crud-page) 16 | 17 | ## Setup 18 | 19 | In this module, we will try to create a Note CRUD application using local database storage. First, we will prepare the main pages: `main.dart` and `notes_page.dart`. 20 | 21 | ```dart 22 | // main.dart 23 | import 'package:flutter/material.dart'; 24 | 25 | void main() async { 26 | 27 | runApp(const MyApp()); 28 | } 29 | 30 | class MyApp extends StatelessWidget { 31 | const MyApp({super.key}); 32 | 33 | // This widget is the root of your application. 34 | @override 35 | Widget build(BuildContext context) { 36 | return const MaterialApp( 37 | debugShowCheckedModeBanner: false, 38 | home: NotesPage() 39 | ); 40 | } 41 | } 42 | 43 | ``` 44 | 45 | ```dart 46 | // pages/notes_page.dart 47 | import 'package:flutter/material.dart'; 48 | 49 | class NotesPage extends StatefulWidget { 50 | const NotesPage({super.key}); 51 | 52 | @override 53 | State createState() => _NotesPageState(); 54 | } 55 | 56 | class _NotesPageState extends State { 57 | 58 | 59 | @override 60 | Widget build(BuildContext context) { 61 | 62 | return Scaffold( 63 | appBar: AppBar(title: const Text('Notes')), 64 | floatingActionButton: FloatingActionButton( 65 | onPressed: createNote, 66 | child: const Icon(Icons.add), 67 | ),); 68 | } 69 | } 70 | ``` 71 | 72 | Next, we will prepare which local database to use. This time, we will try using Isar. You can adjust it with another local database. First, we add the dependencies as follows: 73 | 74 | ```sh 75 | flutter pub add isar isar_flutter_libs path_provider 76 | flutter pub add -d isar_generator build_runner 77 | flutter pub add provider 78 | ``` 79 | 80 | ## Note 81 | 82 | Once the dependencies are ready, we will create the `Note` class using Isar. 83 | 84 | ```dart 85 | // models/note.dart 86 | import 'package:isar/isar.dart'; 87 | 88 | // this line is needed to generate file 89 | // then run dart run build_runner build 90 | part 'note.g.dart'; 91 | 92 | @Collection() 93 | class Note { 94 | Id id = Isar.autoIncrement; 95 | late String text; 96 | } 97 | ``` 98 | 99 | In `Note`, there are only two properties: `id` and `text` as the collection. We need to include `part 'note.g.dart';` so that this file can be modified to run in Flutter. Once Note is ready, please run `dart run build_runner build` in the terminal. 100 | 101 | ## Note Database 102 | 103 | Next, we will create the database. Prepare `note_database.dart`. 104 | 105 | ```dart 106 | // models/note_database.dart 107 | import 'package:crud_local_database_app/models/note.dart'; 108 | import 'package:flutter/cupertino.dart'; 109 | import 'package:isar/isar.dart'; 110 | import 'package:path_provider/path_provider.dart'; 111 | import 'dart:io'; 112 | 113 | class NoteDatabase extends ChangeNotifier{ 114 | static late Isar isar; 115 | 116 | // INIT 117 | 118 | // list 119 | final List currentNotes = []; 120 | 121 | // create 122 | 123 | // read 124 | 125 | // update 126 | 127 | // delete 128 | 129 | } 130 | ``` 131 | 132 | We will implement each part for initializing the database, and the create, read, update, and delete features. 133 | 134 | ### Initialization 135 | 136 | Initialization is needed so that the Isar database can be registered in the device directory location. Please add this code in the INIT section. 137 | 138 | ```dart 139 | // INIT 140 | static Future initialize() async { 141 | if (Platform.isAndroid) { // Check if it's Android 142 | final dir = await getApplicationDocumentsDirectory(); 143 | isar = await Isar.open([NoteSchema], directory: dir.path); 144 | } else { 145 | // Handle other platforms or provide a default directory 146 | final dir = getTemporaryDirectory(); // Example for other platforms 147 | isar = await Isar.open([NoteSchema], directory: (await dir).path); 148 | } 149 | } 150 | 151 | ``` 152 | 153 | ### Create 154 | 155 | Create is a method to add or create a new object (in this case: Note). 156 | 157 | ```dart 158 | // create 159 | Future addNote(String textFromUser) async { 160 | // create a new object 161 | final newNote = Note()..text = textFromUser; 162 | 163 | // save to db 164 | await isar.writeTxn(() => isar.notes.put(newNote)); 165 | 166 | // re-read from db 167 | fetchNotes(); 168 | } 169 | ``` 170 | 171 | This function will process `textFromUser` into the `text` property of `Note`. `isar.writeTxn` is a function to run a write transaction to store `newNote` into Isar’s `notes`. 172 | 173 | ### Read 174 | 175 | `fetchNotes` is a function to retrieve note data from Isar into the `currentNotes` variable, which will be used in the app interface. Here is the code. 176 | 177 | ```dart 178 | // read 179 | Future fetchNotes() async { 180 | List fetchedNotes = await isar.notes.where().findAll(); 181 | currentNotes.clear(); 182 | currentNotes.addAll(fetchedNotes); 183 | notifyListeners(); 184 | } 185 | ``` 186 | 187 | This function will get all notes into the `fetchedNotes` variable and then store them in `currentNotes`, and signal that `currentNotes` has been updated. 188 | 189 | ### Update 190 | 191 | Next is the function to update a note. Here is the code: 192 | 193 | ```dart 194 | // update 195 | Future updateNote(int id, String newText) async { 196 | final existingNote = await isar.notes.get(id); 197 | if (existingNote != null) { 198 | existingNote.text = newText; 199 | await isar.writeTxn(() => isar.notes.put(existingNote)); 200 | await fetchNotes(); 201 | } 202 | } 203 | ``` 204 | 205 | This function will retrieve a `Note` by its `id`. If available, it will update the note using `writeTxn`. Don’t forget to also update `currentNotes` using the `fetchNotes` function. 206 | 207 | ### Delete 208 | 209 | This function is to delete a note based on its id. Don’t forget to also update `currentNotes` using the `fetchNotes` function. 210 | 211 | ```dart 212 | // delete 213 | Future deleteNote(int id) async { 214 | await isar.writeTxn(() => isar.notes.delete(id)); 215 | await fetchNotes(); 216 | } 217 | ``` 218 | 219 | Complete code as follows: 220 | 221 | ```dart 222 | import 'package:crud_local_database_app/models/note.dart'; 223 | import 'package:flutter/cupertino.dart'; 224 | import 'package:isar/isar.dart'; 225 | import 'package:path_provider/path_provider.dart'; 226 | import 'dart:io'; 227 | 228 | class NoteDatabase extends ChangeNotifier{ 229 | static late Isar isar; 230 | 231 | // INIT 232 | static Future initialize() async { 233 | if (Platform.isAndroid) { // Check if it's Android 234 | final dir = await getApplicationDocumentsDirectory(); 235 | isar = await Isar.open([NoteSchema], directory: dir.path); 236 | } else { 237 | // Handle other platforms or provide a default directory 238 | final dir = getTemporaryDirectory(); // Example for other platforms 239 | isar = await Isar.open([NoteSchema], directory: (await dir).path); 240 | } 241 | } 242 | 243 | // list 244 | final List currentNotes = []; 245 | 246 | // create 247 | Future addNote(String textFromUser) async { 248 | // create a new object 249 | final newNote = Note()..text = textFromUser; 250 | 251 | // save to db 252 | await isar.writeTxn(() => isar.notes.put(newNote)); 253 | 254 | // re-read from db 255 | fetchNotes(); 256 | } 257 | // read 258 | Future fetchNotes() async { 259 | List fetchedNotes = await isar.notes.where().findAll(); 260 | currentNotes.clear(); 261 | currentNotes.addAll(fetchedNotes); 262 | notifyListeners(); 263 | } 264 | // update 265 | Future updateNote(int id, String newText) async { 266 | final existingNote = await isar.notes.get(id); 267 | if (existingNote != null) { 268 | existingNote.text = newText; 269 | await isar.writeTxn(() => isar.notes.put(existingNote)); 270 | await fetchNotes(); 271 | } 272 | } 273 | // delete 274 | Future deleteNote(int id) async { 275 | await isar.writeTxn(() => isar.notes.delete(id)); 276 | await fetchNotes(); 277 | } 278 | } 279 | ``` 280 | 281 | ## CRUD Page 282 | 283 | Next, how do we call the database functions in the pages? This can be done using the following commands: 284 | 285 | ```dart 286 | context.read() // returns but no listening 287 | context.watch() // returns and listening 288 | ``` 289 | 290 | These functions can directly call functions from the `NoteDatabase` class. 291 | 292 | We also need to prepare `final textController = TextEditingController();` to store the user’s text input. 293 | 294 | Let’s first create the page structure: 295 | 296 | ```dart 297 | @override 298 | Widget build(BuildContext context) { 299 | // note database 300 | final noteDatabase = context.watch(); 301 | 302 | // current notes 303 | List currentNotes = noteDatabase.currentNotes; 304 | 305 | return Scaffold( 306 | appBar: AppBar(title: const Text('Notes')), 307 | floatingActionButton: FloatingActionButton( 308 | onPressed: createNote, 309 | child: const Icon(Icons.add), 310 | ), 311 | body: ListView.builder( 312 | itemCount: currentNotes.length, 313 | itemBuilder: (context, index) { 314 | // get individual note 315 | final note = currentNotes[index]; 316 | 317 | // list tile UI 318 | return ListTile( 319 | title: Text(note.text), 320 | trailing: Row( 321 | mainAxisSize: MainAxisSize.min, 322 | children: [ 323 | // edit button 324 | IconButton( 325 | onPressed: () => updateNote(note), 326 | icon: const Icon(Icons.edit)), 327 | // delete button 328 | IconButton( 329 | onPressed: () => deleteNote(note.id), 330 | icon: const Icon(Icons.delete)) 331 | ], 332 | ), 333 | ); 334 | }, 335 | )); 336 | } 337 | ``` 338 | 339 | We need `currentNotes` to get the existing notes from the database. Next, we will complete the `createNote`, `updateNote`, `deleteNote`, and `readNote` functions. 340 | 341 | The `createNote` function will create a form and add a note to `NoteDatabase` using the `addNote` function. 342 | 343 | ```dart 344 | // create a note 345 | void createNote() { 346 | showDialog( 347 | context: context, 348 | builder: (context) => AlertDialog( 349 | content: TextField( 350 | controller: textController, 351 | ), 352 | actions: [ 353 | MaterialButton( 354 | onPressed: () { 355 | // add to db 356 | context.read().addNote(textController.text); 357 | 358 | // clear controller 359 | textController.clear(); 360 | 361 | Navigator.pop(context); 362 | }, 363 | child: const Text("Create"), 364 | ) 365 | ], 366 | ), 367 | ); 368 | } 369 | ``` 370 | 371 | The `updateNote` function will take the `Note` to be edited by showing a form and then updating it using the `update` function. 372 | 373 | ```dart 374 | // update a note 375 | void updateNote(Note note) { 376 | textController.text = note.text; 377 | showDialog( 378 | context: context, 379 | builder: (context) => AlertDialog( 380 | title: Text("Update Note"), 381 | content: TextField(controller: textController), 382 | actions: [ 383 | MaterialButton( 384 | onPressed: () { 385 | context 386 | .read() 387 | .updateNote(note.id, textController.text); 388 | // clear controller 389 | textController.clear(); 390 | 391 | Navigator.pop(context); 392 | }, 393 | child: const Text("Update")) 394 | ], 395 | )); 396 | } 397 | ``` 398 | 399 | The `deleteNote` function will directly delete the selected note from the database using the `deleteNote` method. 400 | 401 | ```dart 402 | void deleteNote(int id) { 403 | context.read().deleteNote(id); 404 | } 405 | ``` 406 | 407 | Lastly, there's the `readNotes` function which is used to retrieve data from the database into `currentNotes`. 408 | 409 | ```dart 410 | void readNotes() { 411 | context.read().fetchNotes(); // Use read instead of watch 412 | } 413 | 414 | ``` 415 | 416 | This function will be called at the start of the program execution. You can use `initState()` as follows: 417 | 418 | ```dart 419 | @override 420 | void initState() { 421 | super.initState(); 422 | readNotes(); 423 | } 424 | 425 | ``` 426 | 427 | The complete code as below: 428 | 429 | ```dart 430 | // pages/notes_page.dart 431 | 432 | import 'package:crud_local_database_app/models/note.dart'; 433 | import 'package:crud_local_database_app/models/note_database.dart'; 434 | import 'package:flutter/material.dart'; 435 | import 'package:provider/provider.dart'; 436 | 437 | class NotesPage extends StatefulWidget { 438 | const NotesPage({super.key}); 439 | 440 | @override 441 | State createState() => _NotesPageState(); 442 | } 443 | 444 | class _NotesPageState extends State { 445 | // text controller to access what the user typed 446 | final textController = TextEditingController(); 447 | 448 | @override 449 | void initState() { 450 | super.initState(); 451 | readNotes(); 452 | } 453 | 454 | // create a note 455 | void createNote() { 456 | showDialog( 457 | context: context, 458 | builder: (context) => AlertDialog( 459 | content: TextField( 460 | controller: textController, 461 | ), 462 | actions: [ 463 | MaterialButton( 464 | onPressed: () { 465 | // add to db 466 | context.read().addNote(textController.text); 467 | 468 | // clear controller 469 | textController.clear(); 470 | 471 | Navigator.pop(context); 472 | }, 473 | child: const Text("Create"), 474 | ) 475 | ], 476 | ), 477 | ); 478 | } 479 | 480 | // read notes 481 | void readNotes() { 482 | context.read().fetchNotes(); // Use read instead of watch 483 | } 484 | 485 | // update a note 486 | void updateNote(Note note) { 487 | textController.text = note.text; 488 | showDialog( 489 | context: context, 490 | builder: (context) => AlertDialog( 491 | title: Text("Update Note"), 492 | content: TextField(controller: textController), 493 | actions: [ 494 | MaterialButton( 495 | onPressed: () { 496 | context 497 | .read() 498 | .updateNote(note.id, textController.text); 499 | // clear controller 500 | textController.clear(); 501 | 502 | Navigator.pop(context); 503 | }, 504 | child: const Text("Update")) 505 | ], 506 | )); 507 | } 508 | 509 | // delete a note 510 | void deleteNote(int id) { 511 | context.read().deleteNote(id); 512 | } 513 | 514 | @override 515 | Widget build(BuildContext context) { 516 | // note database 517 | final noteDatabase = context.watch(); 518 | 519 | // current notes 520 | List currentNotes = noteDatabase.currentNotes; 521 | 522 | return Scaffold( 523 | appBar: AppBar(title: const Text('Notes')), 524 | floatingActionButton: FloatingActionButton( 525 | onPressed: createNote, 526 | child: const Icon(Icons.add), 527 | ), 528 | body: ListView.builder( 529 | itemCount: currentNotes.length, 530 | itemBuilder: (context, index) { 531 | // get individual note 532 | final note = currentNotes[index]; 533 | 534 | // list tile UI 535 | return ListTile( 536 | title: Text(note.text), 537 | trailing: Row( 538 | mainAxisSize: MainAxisSize.min, 539 | children: [ 540 | // edit button 541 | IconButton( 542 | onPressed: () => updateNote(note), 543 | icon: const Icon(Icons.edit)), 544 | // delete button 545 | IconButton( 546 | onPressed: () => deleteNote(note.id), 547 | icon: const Icon(Icons.delete)) 548 | ], 549 | ), 550 | ); 551 | }, 552 | )); 553 | } 554 | } 555 | ``` 556 | 557 | In the `main` function, we need to initialize the Isar database at the beginning as follows: 558 | 559 | ```dart 560 | import 'package:crud_local_database_app/models/note_database.dart'; 561 | import 'package:crud_local_database_app/pages/notes_page.dart'; 562 | import 'package:provider/provider.dart'; 563 | import 'package:flutter/material.dart'; 564 | 565 | 566 | void main() async { 567 | WidgetsFlutterBinding.ensureInitialized(); 568 | await NoteDatabase.initialize(); 569 | 570 | runApp( 571 | ChangeNotifierProvider( 572 | create: (context) => NoteDatabase(), 573 | child: const MyApp(), 574 | ) 575 | ); 576 | } 577 | 578 | class MyApp extends StatelessWidget { 579 | const MyApp({super.key}); 580 | 581 | // This widget is the root of your application. 582 | @override 583 | Widget build(BuildContext context) { 584 | return const MaterialApp( 585 | debugShowCheckedModeBanner: false, 586 | home: NotesPage() 587 | ); 588 | } 589 | } 590 | 591 | ``` 592 | 593 | ### Result: 594 | 595 | ![gif](images/hasil.gif) 596 | 597 | If there is an error while running the app, try to replace `subproject` on `android/build.gradle` with this code. 598 | 599 | ```kts 600 | subprojects { 601 | afterEvaluate { project -> 602 | if (project.plugins.hasPlugin("com.android.application") || 603 | project.plugins.hasPlugin("com.android.library")) { 604 | project.android { 605 | compileSdkVersion 34 606 | buildToolsVersion "34.0.0" 607 | } 608 | } 609 | if (project.hasProperty("android")) { 610 | project.android { 611 | if (namespace == null) { 612 | namespace project.group 613 | } 614 | } 615 | } 616 | } 617 | project.buildDir = "${rootProject.buildDir}/${project.name}" 618 | project.evaluationDependsOn(':app') 619 | } 620 | ``` 621 | 622 | You can learn with other databases like Hive and ObjectBox with these references: 623 | 624 | - web article: https://blog.logrocket.com/comparing-hive-other-flutter-app-database-options & https://github.com/o-ifeanyi/db_benchmarks 625 | - Flutter Hive Database Tutorial: https://www.youtube.com/watch?v=FB9GpmL0Qe0 626 | - Flutter ObjectBox Database Tutorial: English version 627 | - Flutter SQFlite Database Tutorial: https://www.youtube.com/watch?v=bihC6ou8FqQ 628 | - Flutter Isar Database Tutorial : https://www.youtube.com/watch?v=jVgQ5esp-PE 629 | --------------------------------------------------------------------------------