Baştan sona Twitter'ı yazalım #4 (Flutter, Riverpod, Fpdart, Appwrite)

Gauloran

Moderasyon Tim Lideri
7 Tem 2013
8,355
897
Blackwell Academy
Merhaba. Bu konu serinin devamıdır:

Baştan sona Twitter'ı yazalım #1 (Flutter, Riverpod, Fpdart, Appwrite)
Baştan sona Twitter'ı yazalım #2 (Flutter, Riverpod, Fpdart, Appwrite)
Baştan sona Twitter'ı yazalım #3 (Flutter, Riverpod, Fpdart, Appwrite)

7u7xwqk.png


fe5sPI.png
fe5OM8.png
fe5Pe1.png


- post oluşturma ekranının yazılması (göstermek için appwrite dan dummy postları eklemiştim uygulama içinden yazıp gösterelim)
- diğer eksik metodların eklenmesi

Kod:
class CreatePostScreen extends ConsumerStatefulWidget {
  static route() => MaterialPageRoute(
        builder: (context) => const CreatePostScreen(),
      );
  const CreatePostScreen({super.key});

  @override
  ConsumerState<ConsumerStatefulWidget> createState() => _CreatePostScreenState();
}

class _CreatePostScreenState extends ConsumerState<CreatePostScreen> {
  final postTextController = TextEditingController();
  List<File> images = [];

  @override
  void dispose() {
    super.dispose();
    postTextController.dispose();
  }

UI tarafında iş görecek olan post ekleme kısmını yazıyoruz. Bunun için Riverpod package kullandığımızdan dolayı stful yazdığınızda zaten consumer stateful widget vscode üzerinde belirir eklersiniz ardından navigasyon kolaylığı olsun diye static route metodunu oluşturuyoruz. postu kullanıcı gireceği için o textfield için bir controller oluşturuyoruz. Ve kullanıcılar postlarına birden fazla resim ekleyebileceğinden bunun için resimleri tutacağımız bir listeye ihtiyacımız var. dispose metodunda da gerekli şeyleri dispose ediyoruz.

Kod:
void sharePost() {
    ref.read(postControllerProvider.notifier).sharePost(
          images: images,
          text: studyTimeController.text.isEmpty
              ? postTextController.text
              : "${studyTimeController.text}\n${postTextController.text}",
          context: context,
          repliedTo: '',
          repliedToUserId: '',
        );

    Navigator.pop(context);
  }

  void onPickImages() async {
    images = await pickImages();
    setState(() {});
  }

7u7xwqk.png


postların paylaşılması için send butonuna basıldığında çalışacak fonksiyon aslında apiyi çalıştıracak olan providerı kullanıyoruz sharePost metodunu tetikleyecek. Metodun gerekli parametrelerini giriyoruz zaten api'yi yazarken o metodu yazdığımız için önceden zaten açıklamıştım.

Kod:
@override
  Widget build(BuildContext context) {
    final currentUser = ref.watch(currentUserDetailsProvider).value;
    final isLoading = ref.watch(postControllerProvider);

    return Scaffold(
      appBar: AppBar(
        leading: IconButton(
          onPressed: () {
            Navigator.pop(context);
          },
          icon: const Icon(Icons.close, size: 30),
        ),
        actions: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: RoundedSmallButton(
              onTap: sharePost,
              label: AppTexts.send,
            ),
          ),
        ],
      ),

Devamında build metodu içerisinde currentUser (şu anki kullanıcıyı çekmek için oluşturduğumuz bir provider) ama kendisi değil değerini alıyoruz. AppBar kısmında IconButton oluşturuyoruz bu üzerinde send yazan bir buton oluşturmanız yeterli bu kısma. Ben paddingde vermişim default olarak. ElevatedButton falan da kullanabilirsiniz istediğinizi RoundedSmallButton widgetını kullanmayı tercih etmişiz. SafeArea widgetını kullanmak bizim için önemli çünkü cihazın UI ile ilgili bir şeyi bölmemesini o çentiği düzeltiyor. CircleAvatar kullanarak currentUser providerı sayesinde şu anki kullanıcının profil fotoğrafını gösteriyoruz. Yuvarlak zaten default şekilde. SizedBox vererek biraz yana boşluk bırakmışız daha sonra kalan alana yayılması için Expanded widgetını kullanarak TextField oluşturmuşuz. Kullanıcının solda profil fotoğrafı olacak sağda gireceği metin yeri olacak yani tıpkı Twitter'daki gibi.

Kod:
SafeArea(
              child: SingleChildScrollView(
                child: Column(
                  children: [
                    Row(
                      children: [
                        const SizedBox(
                          width: 15,
                        ),
                        CircleAvatar(
                          backgroundImage: NetworkImage(
                              currentUser.profilePic == '' ? AssetsConstants.noProfilePicture : currentUser.profilePic),
                          radius: 35,
                        ),
                        const SizedBox(
                          width: 15,
                        ),
                        Expanded(
                          child: Padding(
                            padding: const EdgeInsets.only(right: 12),
                            child: TextField(
                              controller: postTextController,
                              style: const TextStyle(fontSize: 16),
                              decoration: InputDecoration(
                                  hintText: "${AppTexts.whatsHappening}${currentUser.name}?",
                                  hintStyle: const TextStyle(
                                    color: Pallete.greyColor,
                                    fontSize: 16,
                                    fontWeight: FontWeight.w600,
                                  ),
                                  border: InputBorder.none),
                              maxLines: null,
                            ),
                          ),
                        )
                      ],
                    ),


eğer başta bahsettiğimiz resimler için olan liste boş değilse bir CarouselSlider göstererek (yana kaydırmalı resimler) bu resimler map fonksiyonu sayesinde her biri için Image.file döndürüyoruz ve fit olarak BoxFit.cover olarak ayarlıyoruz. border ve borderRadiusları falan artık yazmıyorum her seferinde. Renkleri de Pallete'den çektiğimizi biliyorsunuz.


Kod:
  if (images.isNotEmpty)
                      CarouselSlider(
                        items: images.map(
                          (file) {
                            return Container(
                              decoration: BoxDecoration(
                                  border: Border.all(width: 5, color: Pallete.whiteColor),
                                  borderRadius: BorderRadius.circular(10)),
                              width: 200,
                              margin: const EdgeInsets.symmetric(horizontal: 5),
                              child: Image.file(
                                file,
                                fit: BoxFit.cover,
                              ),
                            );
                          },
                        ).toList(),
                        options: CarouselOptions(
                          height: 200,
                          enableInfiniteScroll: false,
                        ),
                      ),

Bottom kısmı fazla karışık değil

Kod:
  bottomNavigationBar: Container(
        padding: Paddings.bottomLinePadding,
        decoration: const BoxDecoration(border: Border(top: BorderSide(color: Pallete.greyColor, width: 0.3))),
        child: Row(
          children: [
            Padding(
              padding: Paddings.paddingForBottomNavigationIcons,
              child: GestureDetector(onTap: onPickImages, child: const Icon(Icons.photo)),
            ),

7u7xwqk.png


bottomNavigationBar kısmında bir container oluşturup buna dekorasyon verip padding de atayarak (hep yaptığımız şeyler) Tıklanabilir bir widget olması için GestureDetector kullanıyoruz. Tıklandığında resimleri seçmek için çalıştıracağımız fonksiyon zaten Utils'den gelen bir özellik :

Kod:
  void onPickImages() async {
    images = await pickImages();
    setState(() {});
  }

Utilsdeki pickImages:

Kod:
Future<List<File>> pickImages() async {
  List<File> images = [];
  final ImagePicker picker = ImagePicker();
  final imageFiles = await picker.pickMultiImage();
  if (imageFiles.isNotEmpty) {
    for (final image in imageFiles) {
      images.add(File(image.path));
    }
  }
  return images;
}

ImagePicker package kullanarak resim seçme pickMultiImage kullanarak çoklu resim seçme için bize gereken fonksiyonu yazıyoruz.

Hashtaglere tıklandığında gösterilecek UI kısmı

Yine aynı şeyler bu sefer hashtaglere sahip o postların gelmesi için getPostsByHashtagProvider(hashtag)).when kullanıyoruz.

Kod:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:kooginapp/core/common/loading_page.dart';
import 'package:kooginapp/core/constants/app_texts.dart';
import 'package:kooginapp/core/error/error_page.dart';
import 'package:kooginapp/features/posts/controller/post_controller.dart';
import 'package:kooginapp/features/posts/views/post_reply_view.dart';
import 'package:kooginapp/features/posts/widgets/post_card.dart';

class HashtagView extends ConsumerWidget {
  static route(String hashtag) => MaterialPageRoute(
        builder: (context) => HashtagView(hashtag: hashtag),
      );

  final String hashtag;
  const HashtagView({
    super.key,
    required this.hashtag,
  });


  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
        appBar: AppBar(
          title: const Text(AppTexts.hashtagTitle),
        ),
        body: ref.watch(getPostsByHashtagProvider(hashtag)).when(
              data: (posts) {
                return ListView.builder(
                  itemCount: posts.length,
                  itemBuilder: (context, index) {
                    final post = posts[index];
                    return PostCard(
                      post: post,
                      commentability: true,
                      where: () {
                        Navigator.push(
                          context,
                          PostReplyScreen.route(post),
                        );
                      },
                    );
                  },
                );

                /*
           
             */
              },
              error: (error, stackTrace) => ErrorText(error: error.toString()),
              loading: () => const Loader(),
            ));
  }
}

Bize UI tarafında yardımcı olan widget CarouselImage:

Kod:
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:kooginapp/core/providers/providers.dart';
import 'package:kooginapp/core/theme/theme.dart';

class CarouselImage extends ConsumerStatefulWidget {
  final List<String> imageLinks;
  const CarouselImage({super.key, required this.imageLinks});

  @override
  ConsumerState<ConsumerStatefulWidget> createState() => _CarouselImageState();
}

class _CarouselImageState extends ConsumerState<CarouselImage> {

  int _current = 0;
  @override
  Widget build(BuildContext context) {
    final appThemeState = ref.watch(appThemeStateNotifier);
    return Stack(
      alignment: Alignment.center,
      children: [
        Column(
          children: [
            CarouselSlider(
              items: widget.imageLinks.map(
                (link) {
                  return Container(
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(25),
                    ),
                    child: Image.network(
                      link,
                      fit: BoxFit.contain,
                    ),
                  );
                },
              ).toList(),
              options: CarouselOptions(
                viewportFraction: 1,
                height: 200,
                enableInfiniteScroll: false,
                onPageChanged: (index, reason) {
                  setState(() {
                    _current = index;
                  });
                },
              ),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: widget.imageLinks.asMap().entries.map((e) {
                return Container(
                  width: 12,
                  height: 12,
                  margin: const EdgeInsets.symmetric(horizontal: 4),
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: appThemeState.isDarkModeEnabled ? Pallete.whiteColor.withOpacity(
                      _current == e.key ? 0.9 : 0.4,
                    ) : Pallete.blackColor.withOpacity(_current == e.key ? 0.9 : 0.4,),
                  ),
                );
              }).toList(),
            )
          ],
        )
      ],
    );
  }
}

7u7xwqk.png


Hashtaglenebilmesi için kullanıcıların postlarının yaptığımız mantık TextSpan kullanmak. text ve textColor bekleyen bir stless sınıf oluşturup textspan mantığını uyguluyoruz. Tıklandığında HashtagView'a yönlendirme yapacak. Aynı zamanda https:// ve www. ile başlayanların link olarak gözükebilmesi ve mavi renk olabilmesi için de kodumuzu yazıyoruz.

Kod:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:kooginapp/core/theme/theme.dart';
import 'package:kooginapp/features/posts/views/hashtag_view.dart';

class HashtagText extends StatelessWidget {
  final String text;
  final Color textColor;
  const HashtagText({
    super.key,
    required this.text,
    required this.textColor,
  });

  @override
  Widget build(BuildContext context) {
    List<TextSpan> textspans = [];

    text.split(' ').forEach((element) {
      if (element.startsWith('#')) {
        textspans.add(
          TextSpan(
            text: '$element ',
            style: const TextStyle(
              color: Pallete.blueColor,
              fontSize: 14,
              fontWeight: FontWeight.bold,
            ),
            recognizer: TapGestureRecognizer()
              ..onTap = () {
                Navigator.push(context, HashtagView.route(element));
              },
          ),
        );
      } else if (element.startsWith('www.') || element.startsWith('https://')) {
        textspans.add(
          TextSpan(
            text: '$element ',
            style: const TextStyle(
              color: Pallete.blueColor,
              fontSize: 14,
            ),
          ),
        );
      } else if (element.startsWith('@')) {
        textspans.add(
          TextSpan(
            text: '$element ',
            style: const TextStyle(
              color: Pallete.blueColor,
              fontSize: 14,
              fontWeight: FontWeight.bold,
            ),
          ),
        );
      } else {
        textspans.add(
          TextSpan(
            text: '$element ',
            style: TextStyle(
              fontSize: 14,
              color: textColor,
            ),
          ),
        );
      }
    });

    return RichText(
      text: TextSpan(
        children: textspans,
      ),
    );
  }
}

7u7xwqk.png


Bu konuda bahsettiğimiz ve örneğini yaptığımız kavramlar: TextSpan, TextField, StatelessWidget, Consumer Stateful Widget, Slider, ImagePicker package, bottomNavigationBar, GestureDetector, padding, map fonksiyonu, Image sınıfı, BoxFit, RoundedSmallButton, ElevatedButton, SizedBox, Provider, API, Riverpod


fJt5KL.png
 
Son düzenleme:

Helmsys

Ar-Ge Ekibi Kıdemli
16 Mar 2022
1,494
1,654
Adının X olarak değiştiğini görmek için bildirimleri açtık bekliyoruz.
Emeğinize sağlık. Oldukça iyi ve temiz kod yazıyorsunuz.
Landscape modunda da arayüz stabil çalışıyor mu?
 

Gauloran

Moderasyon Tim Lideri
7 Tem 2013
8,355
897
Blackwell Academy
Adının X olarak değiştiğini görmek için bildirimleri açtık bekliyoruz.
Emeğinize sağlık. Oldukça iyi ve temiz kod yazıyorsunuz.
Landscape modunda da arayüz stabil çalışıyor mu?
tesekkur ederim degistirelim onu assetse kismina koogin olarak isimlendirdigim resmi x logosuyla degistirmek yeterli. Landscape de calisiyor bu sekilde gozukuyor

fJOzik.jpg

fJOqdx.jpg


Ayni projeyi temel almis baska eklemeler yaptigim bir projeden ssler
 
Üst

Turkhackteam.org internet sitesi 5651 sayılı kanun’un 2. maddesinin 1. fıkrasının m) bendi ile aynı kanunun 5. maddesi kapsamında "Yer Sağlayıcı" konumundadır. İçerikler ön onay olmaksızın tamamen kullanıcılar tarafından oluşturulmaktadır. Turkhackteam.org; Yer sağlayıcı olarak, kullanıcılar tarafından oluşturulan içeriği ya da hukuka aykırı paylaşımı kontrol etmekle ya da araştırmakla yükümlü değildir. Türkhackteam saldırı timleri Türk sitelerine hiçbir zararlı faaliyette bulunmaz. Türkhackteam üyelerinin yaptığı bireysel hack faaliyetlerinden Türkhackteam sorumlu değildir. Sitelerinize Türkhackteam ismi kullanılarak hack faaliyetinde bulunulursa, site-sunucu erişim loglarından bu faaliyeti gerçekleştiren ip adresini tespit edip diğer kanıtlarla birlikte savcılığa suç duyurusunda bulununuz.