Я пытаюсь создать приложение для задач с помощью Flutter и Firebase. Я не лучший специалист в дизайне, поэтому взял дизайн с Dribbble и пытаюсь скопировать его для своего приложения.
Вот ссылка на дизайн. Ссылка на Dribbble
Вот страница, которую я пытаюсь скопировать: Скриншот страницы
Вот код, который я сделал на данный момент: (на самом деле он не работает)
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final String userID = FirebaseAuth.instance.currentUser!.uid;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 25.0),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(left: 25),
child: Align(
alignment: Alignment.centerLeft,
child: StreamBuilder(
stream: FirebaseFirestore.instance
.collection("Users")
.doc(userID)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return const Text("Erreur");
}
return Text.rich(
TextSpan(
text: "Bonjour,\n",
style: GoogleFonts.exo2(
fontSize: 40,
height: 1.25,
),
children: [
TextSpan(
text: snapshot.data!.data()!["name"],
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
);
},
),
),
),
Row(
children: [
// Column de gauche
SizedBox(
width: MediaQuery.of(context).size.width / 2,
child: ListView(
children: const [],
),
),
// Column de droite
SizedBox(
width: MediaQuery.of(context).size.width / 2,
child: Column(
children: [
Container(),
],
),
),
],
),
],
),
),
),
);
}
}
Я действительно не знаю, как сделать тот же макет, потому что мне нужно разделить экран на две части, и есть ListView.builder (в моем коде есть только ListView), которому нужен фиксированный размер.
Редактировать : Вот как сейчас выглядит мое приложение (потому что я не смог найти решение самостоятельно)
Вот полный код страницы (не обращайте внимания на код Firebase):
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:db_todoapp/components/my_task_tile.dart';
import 'package:db_todoapp/pages/account_page.dart';
import 'package:db_todoapp/utilities/task_service.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shimmer/shimmer.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final String userID = FirebaseAuth.instance.currentUser!.uid;
final TaskService _ts = TaskService();
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 25.0),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(left: 25),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Message de bienvenue
StreamBuilder(
stream: FirebaseFirestore.instance
.collection("Users")
.doc(userID)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return Shimmer.fromColors(
baseColor: const Color.fromRGBO(224, 224, 224, 1),
highlightColor: Colors.grey,
child: Text.rich(
TextSpan(
text: "Bonjour,\n",
style: GoogleFonts.exo2(
fontSize: 40,
height: 1.25,
),
children: const [
TextSpan(
text: "Nom",
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
if (snapshot.hasError) {
return const Text("Erreur");
}
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AccountPage()));
},
child: Text.rich(
TextSpan(
text: "Bonjour,\n",
style: GoogleFonts.exo2(
fontSize: 40,
height: 1.25,
),
children: [
TextSpan(
text: snapshot.data!.data()!["name"],
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
),
);
},
),
// Bouton pour ajouter une tâche
GestureDetector(
onTap: () {
_ts.addTask(context);
},
child: Container(
padding: const EdgeInsets.all(25.0),
decoration: const BoxDecoration(
border: Border(
top: BorderSide(
width: 1.5,
color: Color.fromRGBO(189, 189, 189, 1),
),
bottom: BorderSide(
width: 1.5,
color: Color.fromRGBO(189, 189, 189, 1),
),
left: BorderSide(
width: 1.5,
color: Color.fromRGBO(189, 189, 189, 1),
),
),
borderRadius: BorderRadius.horizontal(
left: Radius.circular(18.0),
),
),
child: Column(
children: [
// Icon +
const Icon(
Icons.add,
size: 30,
),
// Texte
Text(
"Ajouter une tâche",
style: GoogleFonts.exo2(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
],
),
),
)
],
),
),
const Padding(
padding: EdgeInsets.all(25.0),
child: Divider(),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: StreamBuilder(
stream: FirebaseFirestore.instance
.collection("Users")
.doc(userID)
.collection("Tasks")
.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Shimmer.fromColors(
baseColor: const Color.fromRGBO(224, 224, 224, 1),
highlightColor: Colors.grey,
child: GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: null,
itemBuilder: (context, index) {
return MyTaskTile(
taskTitle: "Task Title",
taskIndex: index,
isTaskDone: false,
taskTheme: null,
);
},
),
);
}
if (snapshot.data!.docs.isEmpty ||
snapshot.data == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Illustration
Image.asset("assets/no_task.png"),
// Texte
Text(
"Vous n'avez aucune tâche.",
style: GoogleFonts.exo2(
fontSize: 30,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
],
),
);
}
return GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: snapshot.data?.docs.length ?? 0,
itemBuilder: (context, index) {
return MyTaskTile(
taskTitle: snapshot.data!.docs[index]["title"],
taskIndex: index,
isTaskDone: snapshot.data!.docs[index]["state"],
taskTheme: snapshot.data!.docs[index]["theme"],
);
},
);
},
),
),
)
],
),
),
),
);
}
}
Как я могу заменить GridView.builder и изменить расположение других виджетов, чтобы получить желаемый вид?
Если кто-то знает, как я могу скопировать ту же страницу, или просто поможет мне это сделать, было бы очень здорово!
Как бы вы тут использовали GridView, мне надо добавить кнопку "Добавить задачу" (посмотрите на дизайн).
@Fuiry Я говорю о flutter_staggered_grid_view, а не о GridView. Сначала ознакомьтесь с этим пакетом.
Если я использую «flutter_staggered_grid_view» и укажу кнопку «Добавить задачу» для второго индекса моего построителя, моя кнопка будет прокручиваться, но мне нужна кнопка «Исправить». Поправьте меня, если я ошибаюсь (и извините, если я не совсем понимаю, я не из англоязычной страны)
Итак, вам нужно сделать плавающую кнопку. Тогда мое единственное предложение — использовать размер в шахматной сетке. И покройте всю сетку стопкой. Определите размер этого размера и соответствующим образом расположите кнопку добавления задачи в стеке.
Используйте MasonryGridView.count от flutter_staggered_grid_view. Сделайте crossAxisCount равным 2. И укажите размер элемента. В индекс 1 мы добавим поле размера с предопределенной высотой. Для ширины это будет ширина экрана/2 — интервал и отступ. Теперь у вас есть сайзбокс и его размер. Теперь покройте эту каменную решетку стекой. Поместите кнопку добавления задачи в правом верхнем углу с тем же размером, что и предопределенный размер. И он произведет почти то, что вы хотели. Вам нужно настроить несколько вещей и поэкспериментировать с ними в соответствии с вашими потребностями.
Я только что еще раз отредактировал свой вопрос. Забудьте о проблеме с высотой моего контейнера, с которой я столкнулся, мы исправим ее позже. Итак, как вы можете видеть, мне просто нужно кое-что отредактировать в моем коде, но основа уже готова. Я просто не понимаю, как можно без ошибок оставить пустое место позади позиционированной кнопки. Я пробовал кое-что, но не нашел решения. Надеюсь, вы снова сможете мне помочь. Мне очень жаль, что я заставил вас терять время. Я начинаю с Flutter и изо всех сил стараюсь стать лучше.
Вы использовали GridView. В ответе я упомянул об использовании каменной сетки. А для позиционированной кнопки вы можете обернуть виджет внутри позиционированного виджета другим контейнером, придать ему отступы и цвет. Но это будет выглядеть не очень хорошо, потому что это наложение, и пространство вокруг него будет выглядеть плохо, когда за ним находятся прокручиваемые элементы. И согласно вашему дизайну, кнопка добавления задачи должна находиться поверх других элементов при прокрутке сетки. Я добавлю для вас макет макета, когда найду время, но я бы посоветовал указать упомянутую вами ошибку.
«У меня возникла идея. Я планирую добавить кнопку «Добавить задачу» в первый индекс MasonryGridView. Таким образом, все задачи будут генерироваться с их индексами — 1, что не позволит мне пропустить первая задача моих пользователей. Я буду держать вас в курсе и изменять свои вопросы по мере поиска решения».
Я поделился ответом на свой вопрос. Посмотрите, если вам интересно. Я воспользовался вашими предложениями и адаптировал их под свои конкретные нужды. Еще раз спасибо за ваше время :)





попробуйте использовать GridView.builder, возможно, это решит вашу проблему
Как бы вы использовали здесь GridView.builder? Потому что мне нужно поставить кнопку "Добавить задачу".
тогда вам нужно написать функцию, например, если индекс == 1, затем показать мне контейнер с задачей «Добавить» и вызвать его после построителя
Да, я понимаю, но если я помещу кнопку «Добавить задачу» прямо в GridView.builder, например, под индексом 1, кнопка будет прокручиваться. Мне нужна фиксированная кнопка в моем интерфейсе. И если я скажу «никогда не прокручивать» для GridView.builder, мой список задач также не будет прокручиваться.
GridView.builder — хорошее решение, но я думаю, что для такого точного вида вам нужно иметь два списка рядом друг с другом.
если бы я не использовал два списка рядом друг с другом, дата была бы такой:
пример того, что я хотел:
пример моего кода:
Expanded(
child: Container(
margin:EdgeInsets.only(top: 10) ,
child: ListView(
//listview has default top padding
padding: EdgeInsets.only(top: 0),
children: [
//dates
Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.only(top: 20),
child: Column(
children: [
for (var destination in destenationsList)
// your codee
// your codee
],
),
),
//destinations
Container(
width: s.width*0.8,
margin: EdgeInsets.only(top: 20),
child: Column(
children: [
for (var destination in destenationsList)
// your codee
// your codee
],
),
),
],
),
),]
),
),
),
Да, я думал то же самое, поэтому попробовал использовать ListView.builder, но у меня возникли некоторые ошибки, и я не смог их исправить самостоятельно, поэтому я прошу помощи здесь.
@Furiy У меня почти похожий пример, я отредактирую ответ
Как я уже упоминал в своем комментарии, использование flutter_staggered_grid_view вместе со стеком может создать такой же дизайн. Проверьте код ниже, он дает то, что вы хотели. Но вам нужно соответствующим образом установить размер в коде.
Stack(
children: [
MasonryGridView.count(
padding: const EdgeInsets.all(15.0),
crossAxisCount: 2,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
itemBuilder: (context, index) {
if (index == 1) {
return const SizedBox(
height: 150,
);
}
return Container(
height: 300,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(20.0),
),
);
},
),
Positioned(
right: 0,
top: 15,
child: Container(
height: 150,
width: MediaQuery.sizeOf(context).width * 0.5 - 8, // half of screen size - half of cross axis padding in MasonryGridView will give its width
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
bottomLeft: Radius.circular(20)),
),
alignment: Alignment.center,
child: const Icon(Icons.add),
),
)
],
)
Результат
Хороший ! Это почти то, что я хочу! Попробую воспроизвести это для своего случая. Но есть ли способ избежать наличия красного контейнера за кнопкой?
Красный — это просто цвет, который я придал контейнеру. Вы можете дать ему все, что захотите. Или вы имеете в виду что-то другое, не показывая красный контейнер?
Я имею в виду, что если вы прокрутите вниз, у вас не будет пробела за кнопкой, потому что вы создали пробел в MasonryGridView.count. Итак, есть ли способ всегда иметь пустую область за кнопкой? Это был мой вопрос. Думаю, можно добавить позиционированный пустой контейнер в стек сразу за кнопкой, но я еще не пробовал, потому что работаю над чем-то другим в приложении.
Если вы не хотите, чтобы красный контейнер отображался поверх кнопки при прокрутке, либо оберните кнопку другим контейнером, закрывающим верхнюю часть, и установите для нее белый цвет. Или не добавляйте отступы в верхней части GridView. Все зависит от вашего варианта использования.
Я столкнулся с проблемой. Я попытался установить пробел в первом индексе GridView, но это означает, что первый элемент моего MansoryGridView не появится. Итак, я попытался поместить кнопку «Позиционировано» в контейнер с некоторым отступом и установил цвет контейнера такой же, как и фон моего Scaffold, но, по моему мнению, это выглядит очень плохо. Я столкнулся с другой проблемой: какую бы высоту я ни установил для TaskTile, она остается неизменной в моем MasonryGridView. (Посмотрите мой пост, я отредактирую его, чтобы показать вам).
Было бы хорошо, если бы вы показали какое-нибудь изображение/видео вашего проекта. А представление каменной сетки рассчитывает размер дочернего элемента в зависимости от его содержимого. Если вы не назначите ему какой-либо размер, он примет размер содержимого, но если вы зададите ему некоторый размер, он примет его. И я действительно не понял, что вы имели в виду, говоря, что вы назначаете высоту контейнеру и не имеете над ней контроля.
На самом деле меня нет дома, но я постараюсь объяснить лучше, когда смогу.
Я хотел бы поделиться кодом, который помог мне решить мою проблему:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:db_todoapp/components/my_task_tile.dart';
import 'package:db_todoapp/pages/account_page.dart';
import 'package:db_todoapp/utilities/task_service.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shimmer/shimmer.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final String userID = FirebaseAuth.instance.currentUser!.uid;
final TaskService _ts = TaskService();
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 25.0),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(left: 25),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Message de bienvenue
StreamBuilder(
stream: FirebaseFirestore.instance
.collection("Users")
.doc(userID)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return Shimmer.fromColors(
baseColor: const Color.fromRGBO(224, 224, 224, 1),
highlightColor: Colors.grey,
child: Text.rich(
TextSpan(
text: "Bonjour,\n",
style: GoogleFonts.exo2(
fontSize: 40,
height: 1.25,
),
children: const [
TextSpan(
text: "Nom",
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
if (snapshot.hasError) {
return const Text("Erreur");
}
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AccountPage()));
},
child: Text.rich(
TextSpan(
text: "Bonjour,\n",
style: GoogleFonts.exo2(
fontSize: 40,
height: 1.25,
),
children: [
TextSpan(
text: snapshot.data!.data()!["name"],
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
),
);
},
),
StreamBuilder(
stream: FirebaseFirestore.instance
.collection("Users")
.doc(userID)
.collection("Tasks")
.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const SizedBox();
}
if (snapshot.data == null ||
snapshot.data!.docs.isEmpty) {
return GestureDetector(
onTap: () {
_ts.addTask(context);
},
child: Container(
padding: const EdgeInsets.all(25.0),
decoration: const BoxDecoration(
border: Border(
top: BorderSide(
width: 1.5,
color: Color.fromRGBO(189, 189, 189, 1),
),
bottom: BorderSide(
width: 1.5,
color: Color.fromRGBO(189, 189, 189, 1),
),
left: BorderSide(
width: 1.5,
color: Color.fromRGBO(189, 189, 189, 1),
),
),
borderRadius: BorderRadius.horizontal(
left: Radius.circular(18.0),
),
),
child: Column(
children: [
// Icon +
const Icon(
Icons.add,
size: 30,
),
// Texte
Text(
"Ajouter une tâche",
style: GoogleFonts.exo2(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
return const SizedBox();
},
),
],
),
),
const Padding(
padding: EdgeInsets.all(25.0),
child: Divider(),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: StreamBuilder(
stream: FirebaseFirestore.instance
.collection("Users")
.doc(userID)
.collection("Tasks")
.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {}
if (snapshot.data == null ||
snapshot.data!.docs.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Illustration
Image.asset("assets/no_task.png"),
// Texte
Text(
"Vous n'avez aucune tâche.",
style: GoogleFonts.exo2(
fontSize: 30,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
],
),
);
}
return MasonryGridView.count(
itemCount: snapshot.data!.docs.length + 1,
crossAxisCount: 2,
itemBuilder: (context, index) {
if (index == 0) {
return Padding(
padding: const EdgeInsets.only(right: 10.0),
child: MyTaskTile(
taskTitle: snapshot.data!.docs[index]["title"],
taskIndex: index,
isTaskDone: snapshot.data!.docs[index]["state"],
taskTheme: snapshot.data!.docs[index]["theme"],
),
);
}
if (index == 1) {
return Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: GestureDetector(
onTap: () {
_ts.addTask(context);
},
child: Container(
padding: const EdgeInsets.all(25.0),
decoration: const BoxDecoration(
border: Border(
top: BorderSide(
width: 1.5,
color: Color.fromRGBO(189, 189, 189, 1),
),
bottom: BorderSide(
width: 1.5,
color: Color.fromRGBO(189, 189, 189, 1),
),
left: BorderSide(
width: 1.5,
color: Color.fromRGBO(189, 189, 189, 1),
),
),
borderRadius: BorderRadius.horizontal(
left: Radius.circular(18.0),
),
),
child: Column(
children: [
// Icon +
const Icon(
Icons.add,
size: 30,
),
// Texte
Text(
"Ajouter une tâche",
style: GoogleFonts.exo2(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
);
}
return Padding(
padding: const EdgeInsets.only(right: 10.0),
child: MyTaskTile(
taskTitle: snapshot.data!.docs[index - 1]
["title"],
taskIndex: index - 1,
isTaskDone: snapshot.data!.docs[index - 1]
["state"],
taskTheme: snapshot.data!.docs[index - 1]
["theme"],
),
);
},
);
},
),
),
)
],
),
),
),
);
}
}
Код Firebase не важен; макет — это самое главное.
Спасибо всем, кто помогал мне комментариями/ответами, и спасибо Ultranmus за то, что нашли время мне помочь!
Просто любопытно, вы хотели, чтобы задача добавления накладывалась, верно? Но в вашем коде кнопка является частью сетки и прокручивается.
Этот отзыв предоставляется как часть задачи проверки: Благодарственные письма в stackoverflow не приветствуются. Если вы нашли комментарий полезным, пожалуйста, проголосуйте за него.
Да, вначале я хотел, чтобы кнопка была поверх всего остального, но когда я пытался это сделать, результат меня не удовлетворил. Поэтому я решил просто разместить кнопку в MadonryGridView.
Определите и объявите поток вне метода сборки. В противном случае при каждой сборке виджета будет добавлен новый прослушиватель снимков. И удалите прослушиватель в распоряжении. Для макета, подобного вашему, попробуйте использовать
flutter_staggered_grid_view: ^0.7.0или используйте два списка для двух списков. Создайте их физикуNeverScrollableScrollPhysicsи оберните ее другим представлением списка или представлением одиночной детской прокрутки. Но всегда есть лучший метод, поэтому попробуйте его.