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)
- 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
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.
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.
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.
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.
Bottom kısmı fazla karışık değil
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 :
Utilsdeki pickImages:
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.
Bize UI tarafında yardımcı olan widget CarouselImage:
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.
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
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](https://i.hizliresim.com/7u7xwqk.png)
![fe5sPI.png](https://resmim.net/cdn/2024/04/25/fe5sPI.png)
![fe5OM8.png](https://resmim.net/cdn/2024/04/25/fe5OM8.png)
![fe5Pe1.png](https://resmim.net/cdn/2024/04/25/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](https://i.hizliresim.com/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](https://i.hizliresim.com/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](https://i.hizliresim.com/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](https://i.hizliresim.com/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](https://resmim.net/cdn/2024/05/30/fJt5KL.png)
Son düzenleme: