Best Practices for Clean Code in Flutter

 

Hello,
I’m working on a new mobile app these days. I’ve set myself a deadline and I plan to release my app on GooglePlay at the end of August. The writing part of the program is mostly done, now I’m researching how I can make my code cleaner, more maintainable, how I can refactor it.
At this stage, I wanted to share what I learned here. Now let’s examine what we can do for clean code.

1. Follow Flutter’s Official Guidelines

The first recommendation we came across was Flutter and Dart’s own documentation.

  • Use Dart’s effective practices: Dart’s official guidelines, known as Effective Dart, provide comprehensive advice on how to write readable, maintainable, and effective Dart code. It covers style, documentation, design, and usage guidelines.
  • Flutter documentation: The Flutter documentation includes extensive tutorials, sample codes, and best practices. Regularly refer to it to stay updated on the best ways to implement Flutter features.

2. Structure Your Project

Foldering turned out to be more important than I thought. I used to open and name the dart files under the lib folder respectively, but that’s not how it should be done.

  • Feature-based structure: Instead of organizing files by type (e.g., putting all models in one folder, all views in another), organize your project by features. For instance, have separate directories for features like authenticationhomeprofile. Each feature directory can contain subdirectories for models, views, controllers, etc.
lib/
features/
authentication/
models/
views/
controllers/
home/
models/
views/
controllers
  • Separate business logic: Patterns like BLoC (Business Logic Component), Provider, or Riverpod help you separate business logic from the UI code. This separation makes the code more modular, testable, and maintainable.

3. Use State Management Wisely

It is also necessary to choose the type of state management carefully. We should not say that I learned provider, I will use this in all my projects.

  • Choose appropriate state management: Evaluate different state management solutions such as Provider, Riverpod, BLoC, and Redux. Choose one that fits your project’s needs and complexity. For simpler apps, Provider might be sufficient, while larger apps may benefit from BLoC or Redux.
  • Keep widgets simple: Widgets should primarily be concerned with rendering UI. Avoid embedding business logic within widgets. This keeps your UI code clean and your logic reusable.

4. Write Reusable Widgets

Breaking widgets into small pieces that can be reused is very useful when you want to fix or update the code later. However, it is important not to load more than one mission into a widget, especially about UI.

  • Break down UI into small widgets: Avoid creating large, monolithic widgets. Instead, break down the UI into smaller, reusable widgets that encapsulate specific pieces of functionality. This modularity makes it easier to test and reuse code.
  • Encapsulate widget functionality: Each widget should have a single responsibility. For instance, if you have a ProfileCard widget, it should only be concerned with displaying profile information, not fetching it from a server.

5. Follow Naming Conventions

Naming conventions are the first thing to learn. As the project grows, it needs to be planned so that it doesn’t turn into a tangle, fortunately I’m doing it well for now. :)

  • Consistent naming: Use a consistent naming convention throughout your codebase. For instance, use camelCase for variable names and PascalCase for class names.
  • Meaningful names: Choose descriptive names that convey the purpose of the variable or function. Avoid single-character names except in loop counters.

6. Keep Code DRY (Don’t Repeat Yourself)

That’s what reusable widgets are for.

  • Avoid duplication: If you find yourself writing the same code in multiple places, refactor it into a reusable function or widget. This reduces the risk of bugs and makes maintenance easier.
  • Centralize configurations: Store configuration values (e.g., API endpoints, theme data) in a single place. This makes it easier to update them and ensures consistency.

7. Comment and Document

Writing meaningful lines of commentary to help us remember what was done and where after some time has passed saves lives.

  • Document your code: Write meaningful comments to explain why certain decisions were made, especially for complex or non-obvious code blocks.
  • Use Dartdoc: Add Dartdoc comments to your public APIs to generate useful documentation for your codebase. This helps other developers (and your future self) understand how to use your code.

8. Use Linting and Code Analysis

I first heard about linter in Google Digital Workshop’s Flutter course. Certain rules can be added to the project file and the coding process can be accelerated or automated.

  • Enable linting: Use the Dart analyzer and linter to enforce coding standards and catch potential issues early. The analysis_options.yaml file can be configured to specify linting rules.
  • Customize linter rules: Adjust the linter rules to fit your team’s coding standards. For example, you might enforce a rule that all public methods have documentation comments.

9. Optimize Performance

I may have learned the importance of the const keyword too late, if it doesn’t need to be rebuilt every time, if it doesn’t change, it is a const.

  • Avoid unnecessary rebuilds: Use const constructors, keys, and efficient state management to minimize unnecessary widget rebuilds. For instance, using const constructors can help Flutter determine that a widget tree doesn't need to be rebuilt.
  • Profile your app: Use Flutter’s profiling tools (like the Dart DevTools) to identify and fix performance bottlenecks. This can help you understand where your app spends most of its time and optimize accordingly.

10. Write Tests

I continue to research the test writing phase. I will write a separate article on this.

  • Unit tests: Write unit tests for your business logic. This ensures that your logic works correctly and makes refactoring safer.
  • Widget tests: Write widget tests to ensure your UI behaves as expected. This can catch regressions and ensure your UI works correctly under various conditions.
  • Integration tests: Write integration tests to verify the complete flow of your app. These tests run on real or simulated devices and ensure that all parts of your app work together as expected.

11. Version Control

Git and GitHub are very important for keeping track of project versions. We may want to look retrospectively to see where and what I changed, or we can avoid a possible data loss. After every feature I add in my application, I always send it to git.

  • Use Git effectively: Commit changes frequently with meaningful messages. Use branches for new features and bug fixes to keep the main branch stable.
  • Code reviews: Conduct regular code reviews to maintain code quality and share knowledge among team members. This can catch potential issues early and ensure consistent coding standards.

12. Keep Dependencies Updated

  • Update regularly: Regularly update your Flutter and Dart SDKs, as well as package dependencies. This ensures you have the latest features, bug fixes, and security patches.
  • Check for breaking changes: Review the changelog for breaking changes when updating dependencies. This helps you understand what changes you need to make to your code to accommodate the updates.

13. Use Extensions and Utilities

Helper functions can be used to optimize simple operations. For example, in my last project, I wanted to pluralize the category names coming from the database and I used it in a similar way.

  • Create utility functions: Extract common functionality into utility functions or extensions. This helps keep your code DRY and organized. For example, you might create an extension on String to add a method for capitalizing the first letter
extension StringExtension on String {
String capitalize() {
return this[0].toUpperCase() + substring(1);
}
}

When I searched for what we can do to write clean and sustainable code in Flutter, I gathered what I found under these headings. If you have anything to add, you can write in the comments. I hope it was useful.

Thanks for reading.

Selin.

Flutter’da Temiz Kod için Yapılabilecekler

 

Merhaba,
Bugünlerde yeni bir mobil uygulama üzerinde çalışıyorum. Kendime bir son tarih belirledim ve uygulamamı Ağustos sonunda GooglePlay’de yayınlamayı planlıyorum. Programın yazma kısmı çoğunlukla bitti, şimdi kodumu nasıl daha temiz, daha sürdürülebilir hale getirebileceğimi, nasıl refactor edebileceğimi araştırıyorum.
Bu aşamada öğrendiklerimi burada paylaşmak istedim. Şimdi temiz kod için neler yapabileceğimizi inceleyelim.

1. Flutter’ın Resmi Yönergelerini Takip Edin

Karşılaştığım ilk öneri Flutter ve Dart’ın kendi dokümantasyonu oldu.

  • Dart’ın etkili uygulamalarını kullanın: Dart’ın Effective Dart olarak bilinen resmi yönergeleri, okunabilir, sürdürülebilir ve etkili Dart kodunun nasıl yazılacağı konusunda kapsamlı tavsiyeler sunar. Stil, dokümantasyon, tasarım ve kullanım yönergelerini kapsar.
  • Flutter belgeleri: Flutter belgeleri kapsamlı öğreticiler, örnek kodlar ve en iyi uygulamaları içerir. Flutter özelliklerini uygulamanın en iyi yolları konusunda güncel kalmak için düzenli olarak başvurun.

2. Projenizi Yapılandırın

Klasörlemenin düşündüğümden daha önemli olduğu ortaya çıktı. Dart dosyalarını sırasıyla lib klasörü altında açıp adlandırıyordum, ancak bu şekilde yapılmamalı.

  • Özellik tabanlı yapı: Dosyaları türüne göre düzenlemek yerine (örneğin, tüm modelleri bir klasöre, tüm görünümleri başka bir klasöre koymak), projenizi özelliklere göre düzenleyin. Örneğin, kimlik doğrulama, ev, profil gibi özellikler için ayrı dizinlere sahip olun. Her özellik dizini modeller, görünümler, denetleyiciler vb. için alt dizinler içerebilir.
lib/
features/
authentication/
models/
views/
controllers/
home/
models/
views/
controllers
  • İş mantığını ayırın: BLoC (Business Logic Component), Provider veya Riverpod gibi kalıplar iş mantığını kullanıcı arayüzü kodundan ayırmanıza yardımcı olur. Bu ayırma kodu daha modüler, test edilebilir ve bakımı yapılabilir hale getirir.

3. Durum Yönetimini Akıllıca Kullanın

Durum yönetiminin türünü de dikkatli seçmek gerekir. Ben Provider’ı öğrendim, bunu tüm projelerimde kullanacağım dememeliyiz.

  • Uygun durum yönetimini seçin: Provider, Riverpod, BLoC ve Redux gibi farklı durum yönetimi çözümlerini değerlendirin. Projenizin ihtiyaçlarına ve karmaşıklığına uygun olanı seçin. Daha basit uygulamalar için Provider yeterli olabilirken, daha büyük uygulamalar BLoC veya Redux’tan faydalanabilir.
  • Widget’ları basit tutun: Widget’lar öncelikle kullanıcı arayüzü oluşturma ile ilgilenmelidir. Widget’ların içine iş mantığı gömmekten kaçının. Bu, kullanıcı arayüzü kodunuzu temiz ve mantığınızı yeniden kullanılabilir tutar.

4. Yeniden Kullanılabilir Widget’lar Yazın

Widget’ları yeniden kullanılabilecek küçük parçalara bölmek, kodu daha sonra düzeltmek veya güncellemek istediğinizde çok kullanışlıdır. Ancak, özellikle kullanıcı arayüzü konusunda, bir widget’a birden fazla görev yüklememek önemlidir.

  • Kullanıcı arayüzünü küçük widget’lara bölün: Büyük, monolitik widget’lar oluşturmaktan kaçının. Bunun yerine, kullanıcı arayüzünü belirli işlevsellik parçalarını kapsayan daha küçük, yeniden kullanılabilir widget’lara bölün. Bu modülerlik, kodun test edilmesini ve yeniden kullanılmasını kolaylaştırır.
  • Widget işlevselliğini kapsülleyin: Her widget tek bir sorumluluğa sahip olmalıdır. Örneğin, bir ProfileCard widget’ınız varsa, yalnızca profil bilgilerini görüntülemekle ilgilenmeli, bir sunucudan getirmemelidir.

5. İsimlendirme Kurallarına Uyun

İsimlendirme kuralları öğrenilmesi gereken ilk şey. Proje büyüdükçe arapsaçına dönmemesi için planlanması gerekiyor, neyse ki şimdilik bunu iyi yapıyorum :)

  • Tutarlı adlandırma: Kod tabanınız boyunca tutarlı bir adlandırma kuralı kullanın. Örneğin, değişken isimleri için camelCase ve sınıf isimleri için PascalCase kullanın.
  • Anlamlı isimler: Değişken veya fonksiyonun amacını ifade eden açıklayıcı isimler seçin. Döngü sayaçları dışında tek karakterli isimlerden kaçının.

6. Kendinizi Tekrar Etmeyin
Yeniden kullanılabilir widget’lar bunun için var.

  • Yinelemelerden kaçının: Kendinizi aynı kodu birden fazla yerde yazarken bulursanız, yeniden kullanılabilir bir işlev veya widget’a dönüştürün. Bu, hata riskini azaltır ve bakımı kolaylaştırır.
  • Yapılandırmaları merkezileştirin: Yapılandırma değerlerini (ör. API uç noktaları, tema verileri) tek bir yerde saklayın. Bu, onları güncellemeyi kolaylaştırır ve tutarlılığı sağlar.

7. Yorum Satırları

Aradan zaman geçtikten sonra neyin nerede yapıldığını hatırlamamıza yardımcı olacak anlamlı yorum satırları yazmak hayat kurtarır.

  • Kodunuzu belgeleyin: Özellikle karmaşık veya açık olmayan kod blokları için belirli kararların neden alındığını açıklayan anlamlı yorumlar yazın.
  • Dartdoc kullanın: Kod tabanınız için faydalı belgeler oluşturmak üzere genel API’lerinize Dartdoc yorumları ekleyin. Bu, diğer geliştiricilerin (ve gelecekteki kendinizin) kodunuzu nasıl kullanacaklarını anlamalarına yardımcı olur.

8. Linting ve Kod Analizini Kullanın

Linter’ı ilk olarak Google Digital Workshop’un Flutter kursunda duymuştum. Proje dosyasına belirli kurallar eklenerek kodlama süreci hızlandırılabilir ya da otomatikleştirilebilir.

  • Linting’i etkinleştirin: Kodlama standartlarını uygulamak ve olası sorunları erkenden yakalamak için Dart analizörünü ve linter’ı kullanın. Analysis_options.yaml dosyası, linting kurallarını belirtmek için yapılandırılabilir.
  • Linter kurallarını özelleştirin: Linter kurallarını ekibinizin kodlama standartlarına uyacak şekilde ayarlayın. Örneğin, tüm genel yöntemlerin belge açıklamalarına sahip olması kuralını uygulayabilirsiniz.

9. Performansı Optimize Edin

const anahtar kelimesinin önemini çok geç öğrenmiş olabilirim, her seferinde yeniden oluşturulması gerekmiyorsa, değişmiyorsa kesinlikle const kullanılması gerekir.

  • Gereksiz rebuildlerden kaçının: Gereksiz widget buildlerini en aza indirmek için const yapıcıları, anahtarları ve verimli durum yönetimini kullanın. Örneğin, const kullanmak Flutter’ın bir widget ağacının yeniden oluşturulmasına gerek olmadığını belirlemesine yardımcı olabilir.
  • Uygulamanızın profilini çıkarın: Performans darboğazlarını belirlemek ve düzeltmek için Flutter’ın profil oluşturma araçlarını (Dart DevTools gibi) kullanın. Bu, uygulamanızın zamanının çoğunu nerede geçirdiğini anlamanıza ve buna göre optimize etmenize yardımcı olabilir.

10. Testler Yazın

Test yazım aşamasını araştırmaya devam ediyorum. Bu konuda ayrı bir yazı yazacağım.

  • Birim testleri: İş mantığınız için birim testleri yazın. Bu, mantığınızın doğru çalışmasını sağlar ve refactoring’i daha güvenli hale getirir.
  • Widget testleri: Kullanıcı arayüzünüzün beklendiği gibi davrandığından emin olmak için widget testleri yazın. Bu, regresyonları yakalayabilir ve kullanıcı arayüzünüzün çeşitli koşullar altında doğru çalışmasını sağlayabilir.
  • Entegrasyon testleri: Uygulamanızın tüm akışını doğrulamak için entegrasyon testleri yazın. Bu testler gerçek veya simüle edilmiş cihazlarda çalışır ve uygulamanızın tüm parçalarının beklendiği gibi birlikte çalışmasını sağlar.

11. Versiyon Kontrolü

Git ve GitHub, proje sürümlerini takip etmek için çok önemlidir. Nerede ne değiştirdim diye geriye dönük bakmak isteyebiliriz ya da olası bir veri kaybının önüne geçebiliriz. Ben de uygulamama eklediğim neredeyse her özellikten sonra mutlaka git’e gönderiyorum.

  • Git’i etkin kullanın: Değişiklikleri anlamlı mesajlarla sık sık commit edin. Ana dalı kararlı tutmak için yeni özellikler ve hata düzeltmeleri için dallar kullanın.
  • Kod incelemeleri: Kod kalitesini korumak ve ekip üyeleri arasında bilgi paylaşımını sağlamak için düzenli kod incelemeleri yapın. Bu, olası sorunları erkenden yakalayabilir ve tutarlı kodlama standartları sağlayabilir.

12. Bağımlılıkları Güncel Tutun

  • Düzenli olarak güncelleyin: Flutter ve Dart SDK’larınızın yanı sıra paket bağımlılıklarını da düzenli olarak güncelleyin. Bu, en son özelliklere, hata düzeltmelerine ve güvenlik yamalarına sahip olmanızı sağlar.
  • Son değişiklikleri kontrol edin: Bağımlılıkları güncellerken kırılma değişiklikleri için değişiklik günlüğünü inceleyin. Bu, güncellemelere uyum sağlamak için kodunuzda hangi değişiklikleri yapmanız gerektiğini anlamanıza yardımcı olur.

13. Uzantıları ve Yardımcı Programları Kullanın

Yardımcı fonksiyonlar basit işlemleri optimize etmek için kullanılabilir. Örneğin son projemde veritabanından gelen kategori isimlerini çoğul hale getirmek istemiştim ve benzer şekilde kullanmıştım.

  • Yardımcı fonksiyonlar oluşturun: Ortak işlevleri yardımcı fonksiyonlara veya uzantılara ayıklayın. Bu, kodunuzun DRY ve düzenli kalmasına yardımcı olur. Örneğin, String üzerinde bir uzantı oluşturarak ilk harfi büyük yazmak için bir yöntem ekleyebilirsiniz
extension StringExtension on String {
String capitalize() {
return this[0].toUpperCase() + substring(1);
}
}

Flutter’da temiz ve sürdürülebilir kod yazmak için neler yapabiliriz diye araştırdığımda bulduklarımı bu başlıklar altında topladım. Sizin de eklemek istedikleriniz varsa yorumlara yazabilirsiniz. Umarım faydalı olmuştur.

Okuduğunuz için teşekkürler.

Selin.

print and debugPrint in Flutter

 

test with print and debugPrint

Hello,

When I was looking at the do’s & dont’s posts about Flutter, there was a topic I came across: print and debugPrint. I use the console screen as much as putting breakpoints while writing a program to test with print, to see where I am in stages. However, if I proceed with print, I get the warning “Don’t invoke ‘print’ in production code”. If I write the same line with debugPrint, this warning disappears. So today I wanted to investigate the difference between these two, why one gives a warning and the other doesn’t. Let’s examine both in detail.

What is print?

print is the standard output function of the Dart language. It prints the text specified in the terminal or console.

void main() {
print("This is an example of the print function.");
}

What are the disadvantages?

  • The first disadvantage is that when we want to print long texts or too many lines of output, it can give an “output truncated” error in the console (this has happened to me before, I encountered it when I researched debugPrint). So, we can have problems when printing a lot of data at the same time.
  • The second issue is actually related to the first one. When we want to output a lot of data or a large amount of data, there can be a performance loss. So it is less efficient in terms of performance.

What is debugPrint?

debugPrint is a Flutter framework specific function optimized for Flutter applications. It prints text to the console in the same way, but prevents long text from being split and truncated.

void main() {
debugPrint("This is an example of the debugPrint function.");
}

What are the advantages?

  • The debugPrint function checks the line boundaries of text and splits the output into chunks if necessary. This way, long texts are printed without being properly truncated.
  • This makes debugPrint more performant when printing large data and does not negatively affect the performance of the application.
  • debugPrint limits the output to 800 characters by default. However, we can use debugPrint’s “wrapWidth” parameter for longer output.

wrapWidth Usage

The wrapWidth parameter of the debugPrint function allows us to control how long text is wrapped. Here wrap means to determine after how many characters each line is divided.

String longText = 'This is an example of a very long text.' * 20;  //This means printing the text 20 times in a row.

// debugPrint without wrapWidth (standard)
debugPrint("without wrapWidth:", wrapWidth: 800);
debugPrint(longText);

// usage of debugPrint with wrapWidth parameter (split after the 50th character)
debugPrint("\nwith wrapWidth (50 characters):", wrapWidth: 800);
debugPrint(longText, wrapWidth: 50);
  • Without wrapWidth: debugPrint is used with the default wrap width (800 characters).
  • With wrapWidth (50 Characters): the wrapWidth parameter is set to 50 characters, so that each line is split after 50 characters.

Below we will see the output of these codes: texts will move to a new line after the specified number of characters. This is particularly useful for making long log messages more readable.

without wrapWidth:
This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.This is an example of a very long text.

with wrapWidth:
This is an example of a very long text.This is an
example of a very long text.This is an example of
a very long text.This is an example of a very long
text.This is an example of a very long text.This
is an example of a very long text.This is an
example of a very long text.This is an example of
a very long text.This is an example of a very long
text.This is an example of a very long text.This
is an example of a very long text.This is an
example of a very long text.This is an example of
a very long text.This is an example of a very long
text.This is an example of a very long text.This
is an example of a very long text.This is an
example of a very long text.This is an example of
a very long text.This is an example of a very long
text.This is an example of a very long text.

Which one to use in which situation?

Short and Simple Printouts: For simple and short outputs we can still use print (if we don’t mind the yellow warning).
Long and Complex Output: When you need to print long text or a lot of data, it is wiser to use debugPrint.
General Use: It is still generally more convenient and reliable to use debugPrint when debugging Flutter projects.

As we can see, small changes in the application can make a big difference in performance and usability. After doing my research, I decided to use debugPrint instead of print in all my tests and got rid of the yellow warning. I hope this was useful.

Thank you in advance for your valuable feedback.

Selin.