Simple ToDo Application with Provider

 

Hello,
Today I would like to talk about and illustrate a structure that I found very difficult, complex and took a lot of time to understand during my Flutter learning process. Our topic is the Provider structure in Flutter. I have previously talked about the “State” logic in Flutter, the differences between stateful and stateless widgets. I leave a reminder link below:

Flutter and Widget Concept

We use Stateful Widgets if there will be changes in the application as a result of user interaction or for other reasons. That is, at points that should contain a State object. And with very simple logic, if there is a “State” in the middle, it needs to be managed, that is, we need to choose and use one of the State Management methods so that we can track the state of the State and instantly show the changing interface to the user as a result of interaction. I want to talk about the Provider structure, which is one of these methods. Let’s start if we are ready.

What is Provider ?

Provider is a package used for state management in Flutter, and is used to manage and share state in specific regions or across the application. Provider provides a simple and efficient way to recreate, update and share state.

The encyclopaedic information I used above can be found everywhere. I will show how to use it with a short and simple todo application.

When our project is completed, it will look like the following:

Basic ToDoApp with Provider

Step 1:

First, we create and name a new Flutter application. After the project is created, we open the pubspec.yaml file and add the Provider package here.

dependencies:
flutter:
sdk: flutter
provider: ^6.0.0 //The version number may change
//according to the date you created the application.

Step 2:

The second step is to create a model representing the state. Since our application is TodoApp, we can create a Todo class:

class Todo {
//We will check the todo items with checkbox.
String title;
bool isDone;

Todo({
required this.title,
this.isDone = false, //checkbox will initially come unchecked
});

void toggleDone() {
isDone = !isDone; //method to add tick if there is no tick
//when checkbox is clicked and delete if there is tick
}
}

Step 3:

In the next stage, we need to create a class that manages State and extend it with “ChangeNotifier”. ChangeNotifier is a class that listens and notifies state changes. By calling the notifyListeners() method of this class, we notify this change to all components (widgets) that listen for changes. If we go from our example, let’s say the user wanted to cross out an item he added to the Todo list and ticked the checkbox, thanks to this class, the change is notified to the user interface instantly and the checkbox tick appears and the item is crossed out.

class TodoProvider with ChangeNotifier {
List<Todo> _todos = [];

List<Todo> get todos => _todos;

void addTodo(String title) { //Method to add from CRUD operations
_todos.add(Todo(title: title));
notifyListeners(); //method that allows changes to be reflected
//to the interface instantly
}

void toggleTodoStatus(int index) {
_todos[index].toggleDone();
notifyListeners();
}
}

Step 4:

In this step, we integrate the Provider structure into our main.dart file. ChangeNotifierProvider creates an instance of the ChangeNotifier class (we can think of it as getting an instance) and declares State to subcomponents.

void main() {
runApp(
ChangeNotifierProvider(
create: (context) => TodoProvider(),
child: MyApp(),
),
);
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: TodoListScreen(),
);
}
}

Step 5:

Finally, we create another file called todoListScreen.dart and redirect the MaterialApp’s home page to it.

class TodoListScreen extends StatelessWidget {
final TextEditingController _controller = TextEditingController();

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('To-Do List'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(labelText: 'New To-Do'),
),
),
IconButton(
icon: Icon(Icons.add),
onPressed: () {
if (_controller.text.isNotEmpty) {
Provider.of<TodoProvider>(context, listen: false)
.addTodo(_controller.text);
_controller.clear();
}
},
),
],
),
),
Expanded(
child: Consumer<TodoProvider>(
builder: (context, todoProvider, child) {
return ListView.builder(
itemCount: todoProvider.todos.length,
itemBuilder: (context, index) {
final todo = todoProvider.todos[index];
return ListTile(
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isDone
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
trailing: Checkbox(
value: todo.isDone,
onChanged: (value) {
todoProvider.toggleTodoStatus(index);
},
),
);
},
);
},
),
),
],
),
);
}
}

There are two uses I need to explain here. The first one is Consumer.

Expanded widget with Consumer is the place where todo items will be displayed. Here, the Consumer allows us to display the todo items right below when the user adds an item. Because Consumer listens to the provider (TodoProvider) using the builder method and automatically rebuilds the widget when the state changes.

The second use is the Provider.of<T>(context) construct. This method accesses the provider of the specified type, which in our example is TodoProvider. Here we have a method called addTodo and using this method new todo items can be added to the list. Here we want to access only the state without listening to the state change, so we give the listen parameter as false. If we give true, it also listens to these changes, but it may cause performance problems because it is unnecessary here.

Finally, we have experienced state management by combining the simple To-Do List application with the Provider structure. We can further develop this project, add persistent data storage or more advanced user interface features.

I tried to explain the Provider structure in a very short and simple way. I hope it was useful.

Good coding to all of us.

Selin.

Provider ile Basit ToDo Uygulaması

 

Merhaba,

Bugün sizlere Flutter öğrenme sürecimde beni epey zorlayan, karmaşık bulduğum ve anlamak için oldukça fazla zaman ayırdığım bir yapıdan bahsetmek ve bunu örneklemek istiyorum. Konumuz Flutter’da Provider yapısı. Daha önce Flutter’da “State” mantığından, stateful ve stateless widget’ların farklılıklarından söz etmiştim. Aşağıya hatırlatma linki bırakıyorum:

Flutter ve Widget Kavramı

Stateful Widget’ları uygulamada kullanıcı etkileşimi sonucu veya başka sebeplerle değişiklik olacaksa kullanıyoruz. Yani State nesnesi içermesi gereken noktalarda. Ve çok basit mantıkla eğer ortada bir “State” var ise, bunun yönetilmesi gerekir yani State Management yöntemlerinden birini seçmemi ve kullanmamız gerekir ki State’in durumunu takip edebilelim ve etkileşim sonucu değişen arayüzü kullanıcıya anlık olarak gösterebilelim. Ben size bu yöntemlerden biri olan Provider yapısından bahsetmek istiyorum. Hazırsak başlayalım.

Provider Nedir?

Provider, Flutter’da state management için kullanılan bir paket olup, uygulamanın belirli bölgelerinde veya genelinde durumu (state) yönetmek ve bu durumu paylaşmak için kullanılır. Provider, durumun yeniden oluşturulması, güncellenmesi ve paylaşılması konusunda basit ve etkili bir yol sunar.

Yukarıda kullandığım ansiklopedik bir bilgi, her yerde bulunabilir. Ben kısa ve basit bir todo uygulaması ile açıklamalı olarak bunun nasıl kullanılacağını göstereceğim.

Projemiz tamamlandığında aşağıdaki gibi görünecek:

Provider ile Basit ToDoApp

1. Adım:

İlk olarak yeni bir Flutter uygulaması oluşturup isimlendiriyoruz. Proje oluştuktan sonra pubspec.yaml dosyasını açarak buraya Provider paketini ekliyoruz.

dependencies:
flutter:
sdk: flutter
provider: ^6.0.0 //versiyon numarası
//sizin uygulamayı oluşturduğunuz
//tarihe göre değişebilir.

2. Adım:

İkinci adım olarak durumu (state) temsil eden bir model oluşturuyoruz. Uygulamamız TodoApp olduğu için bir Todo sınıfı oluşturabiliriz:

class Todo {
//todo maddelerini checkbox ile kontrol edeceğiz.
String title;
bool isDone;

Todo({
required this.title,
this.isDone = false, //checkbox başlangıçta işaretlenmemiş olarak gelecek
});

void toggleDone() {
isDone = !isDone; //checkbox tıklandığında tick yok ise eklenmesini,
//var ise silinmesini sağlayan metod
}
}

3. Adım:

Bir sonraki aşamada State’i yöneten bir sınıf oluşturmamız gerekiyor ve “ChangeNotifier” ile genişletiyoruz. ChangeNotifier, durum değişikliklerini dinleyen ve bildiren bir sınıftır. Bu sınıfa ait notifyListeners() isimli metodunu çağırarak d,a değişiklikleri dinleyen tüm bileşenlere (widget'lara) bu değişikliği bildirmiş oluyoruz. Örneğimizden gidecek olursak, kullanıcı Todo listesine eklediği bir maddenin üzerini çizmek istedi ve checkbox’ı işaretledi diyelim, bu sınıf sayesinde kullanıcı arayüzüne değişiklik anında bildiriliyor ve checkbox tick’i beliriyor ve maddenin üzeri çiziliyor.


class TodoProvider with ChangeNotifier {
List<Todo> _todos = [];

List<Todo> get todos => _todos;

void addTodo(String title) { //CRUD işlemlerinden eklemeyi sağlayan metot
_todos.add(Todo(title: title));
notifyListeners(); //değişikliklerin anlık olarak arayüze yansıtılmasını
//sağlayan metot
}

void toggleTodoStatus(int index) {
_todos[index].toggleDone();
notifyListeners();
}
}

4. Adım:

Bu adımda main.dart dosyamıza Provider yapısını entegre ediyoruz. ChangeNotifierProvider, ChangeNotifier sınıfının bir örneğini oluşturuyor (instance almak gibi düşünebiliriz) ve alt bileşenlere State bildirimi yapıyor.

void main() {
runApp(
ChangeNotifierProvider(
create: (context) => TodoProvider(),
child: MyApp(),
),
);
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: TodoListScreen(),
);
}
}

5. Adım:

Son olarak todoListScreen.dart adında bir dosya daha oluşturuyoruz ve MaterialApp’in home sayfasını buna yönlendiriyoruz.

class TodoListScreen extends StatelessWidget {
final TextEditingController _controller = TextEditingController();

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('To-Do List'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(labelText: 'New To-Do'),
),
),
IconButton(
icon: Icon(Icons.add),
onPressed: () {
if (_controller.text.isNotEmpty) {
Provider.of<TodoProvider>(context, listen: false)
.addTodo(_controller.text);
_controller.clear();
}
},
),
],
),
),
Expanded(
child: Consumer<TodoProvider>(
builder: (context, todoProvider, child) {
return ListView.builder(
itemCount: todoProvider.todos.length,
itemBuilder: (context, index) {
final todo = todoProvider.todos[index];
return ListTile(
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isDone
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
trailing: Checkbox(
value: todo.isDone,
onChanged: (value) {
todoProvider.toggleTodoStatus(index);
},
),
);
},
);
},
),
),
],
),
);
}
}

Burada açıklamam gereken iki kullanım var. Birincisi Consumer.

Consumer’ın yer aldığı Expanded widget’ı todo maddelerinin görüntüleneceği yeri oluşturuyor. Burada Consumer bize kullanıcı bir madde eklediğinde hemen altında görüntülenmesini sağlıyor. Çünkü Consumer, builder metodunu kullanarak, sağlayıcıyı (TodoProvider) dinliyor ve durum değiştiğinde otomatik olarak widget'ı yeniden oluşturuyor.

İkinci kullanım ise Provider.of<T>(context) yapısı. Bu metod belirtilen türdeki provider’a ulaşıyor, ki bu bizim örneğimizde TodoProvider. Burada addTodo isimli bir metodumuz var ve bu metodu kullanarak listeye yeni todo maddeleri eklenebiliyor. Burada durum değişikliğini dinlemeden sadece duruma erişmek istediğimizden listen parametresini false olarak veriyoruz. true verirsek bu değişiklikleri de dinler ancak burada gereksiz olduğundan performans sorunlarına yol açabilir.

Son olarak, basit To-Do Listesi uygulaması ile Provider yapısını birleştirerek state management (durum yönetimi) konusunu deneyimlemiş olduk. Bu projeyi daha da geliştirebilir, kalıcı veri depolama veya daha gelişmiş kullanıcı arayüzü özellikleri ekleyebiliriz.

Provider yapısını oldukça kısa ve basit şekliyle anlatmaya çalıştım. Umarım faydası olmuştur.

Hepimize iyi kodlamalar.

Selin.

GitHub and Simple Commands

 

Hello,
This week I am starting the Git and Github series. In my first article, I will define basic commands and terminology. In the following articles, I will cover advanced git commands and GitHub Desktop operations.

Git & GitHub & GitHub Desktop

Git is a version control system.
GitHub is where we store our code and collaborate with others. It serves to store open source code. It allows two different codes of a project to be merged.
GitHub Desktop helps us work with GitHub locally.

Why should we use GitHub?

  • The most widely used version control system in the world.
  • It is free for individual users and cheap for teams and companies.
  • We can follow software developers on the platform, make comments and bug reports on open source developed codes.
  • Thanks to forking, we can contribute to the development of open source codes.

GitHub Terminology

  • Repository: We can liken it to a folder or repository. It contains all the files of the project and the revision history of each file.
  • Branch: The version of the project separated from the main work. If we think of the main project as a tree trunk, we can liken the branch, which is the side version, to the branch or branches of the tree. A project can have more than one branch.
  • Fork: A copy of a repository in a github account. It allows us to freely experiment with changes without affecting the original project. We can fork any public repository on our computer.
  • Clone: The process of copying a repository on GitHub to the local computer.
  • Main/Master: The main branch of a repository. In older GitHub versions it is called master, in newer versions it is called main.
  • Pull: The process of withdrawing all or a certain part of a repository to a computer (local).
  • Push: Sending the changes made in the project files to the github servers.
  • Merge: Merging a branch on a repository with another branch. Conflict may occur at the end of the process.
  • Conflict: Conflict when merging two branches. Such as different codes written on the same lines. Some of them GitHub resolves automatically, some need to be resolved manually.
  • Pull Request: GitHub’s way of notifying interested parties about your request to include your changes in the relevant branch.
  • Stash: Keeping the changes made while navigating between two branches without committing, storing them, and recalling them when necessary.
  • Discard: Deleting changes made while navigating between two branches.
  • Remote: Assets, files, etc. on GitHub servers.
  • Local: Entities within our local computer.

Simple Commands

git clone repository url written to copy a repository from remote to local

git remote add origin repository url : to synchronise a remote repository with the local repo.

git push -u origin main : after the change, we write it so that we can resend it to remote.

If there is a modified repo in remote (it may have been modified on another computer or GitHub page), we use fetch and pull commands to pull it to local.

git fetch : pulls the changes from remote but does not show it in local files, it just writes it into a hidden git file.

git pull : We execute this command so that it is also reflected in the local files.

git branch -a : lists all branches, both remote and local.

git branch : lists only the branches located locally.

git checkout -b branchname : creates a new branch and switches to this branch with a single command.

git checkout branchname : switches to the existing branch whose name is written in the command.

git add . / git add -A : allows to index all files.

git add filename : only indexes the named file.

git commit -m “message : It is used to upload the files added to the directory or those that have been modified to the local repository. Since the added message will appear on the screen where the commits are listed, it is important to write the information about what kind of change was made in this field so that it can be understood correctly when the project is examined later.

git push -u origin branchname : Adds the named branch opened in local to remote. Now main and the named branch appear separately in remote. After this stage, if desired, this branch and main can be merge.Açılan yeni branch’ta yapılan değişiklikleri main/master’a taşıma mergekomutu ile yapılır. Branch’ı main’e taşıyabilmek için:

  • git checkout main : switches to main.
  • git merge origin/branch name hosting the change : moves commits added to branch to main.
  • git push : The process is completed by sending the latest version to GitHub.
  • Merge can alternatively be done in the GitHub interface. Suppose we made a change in the branch and committed this change to GitHub. When the relevant branch is selected on the GitHub page, the following screen appears.

The warning at the top means that branch is 1 commit ahead of main. Here, when we click the “Open pull request” button, the first thing we will pay attention to on the screen that appears is the base and compare fields.

Since we want to add the changes made in the branch to main, it should be base: main and compare: branch.

When we click the “Create pull request” button by adding a description, if there is no conflict between branch and main, the “Merge pull request” button becomes active. Click “Confirm merge” by adding a new description. With this, both sections are synchronised. After this stage, the branch can be deleted if desired. We can do this with the “Delete branch” button on the same page.

NOT: Since this merge operation using the GitHub interface is done remotely, no changes are displayed in the local files. In order to do this, we need to pull the latest state of the project to our computer with the git pull command.

One of the reasons for conflict is that different codes are written on the same line number of the same file in different branches.

Changes are committed and pushed separately in both branches. Afterwards, when we switch to main and want to merge as I described above, we get an error telling us that conflict occurs, automatic correction cannot be done, conflicts should be committed after they are corrected. When we return to the IDE we are using (Visual Studio Code, Atom, etc.), options about which lines conflict occurred and what can be done about it appear.

Conflict View in Visual Studio Code
  • Accept Current Change
  • Accept Incoming Change
  • Apply Both Changes
  • Compare Changes

After selecting which one we want to apply (one of the first 3 options), we can perform the commit & push operation. The relevant change is made in Main. Nothing changes in the other branch. If we come to the branch and do the same merge operation here (reverse merge), the two branches will be merged successfully since there will be no conflict with the current version of the main file.

git stash : storing a change without committing it, keeping it in local memory. When is it used? Let’s say there are parts of code that don’t work in our file. At the same time, working changes will come from another branch. Before merging these changes with our file, we hide the non-working parts with stash. We get the changes coming from the other branch with mergeand then with git stash pop, we bring back our codes that we keep temporarily in local memory. With git stash apply, we both assign our codes to local memory and keep them visible.

git restore . : deletes all uncommitted or unstashed changes.

git reset --hard : Returns to the last commit, deletes all other changes, but the changes stored with stash continue to be kept in memory. In other words, when we run the git stash apply command, the stored changes come back. This command is valid for changes made before commit. Even if it is not pushed, the committed changes are kept in the record.

git revert commit id : A very long commit id is created in every commit operation we do. We can see this with the git log command or from the screen where commits are listed in the GitHub interface. When we run the revert command with this id, our project returns to the moment that commit was made. But while doing this, it does not delete the changes made in the meantime, instead it creates a new commit with a reverse change and returns it back to the relevant commit. And when we open the log screen again, we see that the message of the last commit starts with the REVERTkeyword.

That’s all the information from our Git series. I would be very pleased if you support me by reading my articles for more. I hope it will be useful.

Thank you for your reading.

Selin.