본인은 게시판에 게시할 이미지를 불러오는 코드를 구현할 예정이다.
이미지는 5장으로 한정하며 완성된 UI는 위 사진과 같다
먼저 본인은 게시판, 채팅, 유저 정보 등 여러 화면에서 사용할 예정이므로 Service를 싱글톤으로 구현했다
pickImage() 메소드는 여러장의 사진을 불러오는 메소드이며, picksingleImage()는 한장의 사진을 불러오는 메소드이다.
다음은 게시판에서 서비스를 사용하는 Provider이다
copilot이 작성해준 주석...
참고로 위 코드에서 리스트를 복사하는 부분이 있는데
list = super.state
list.remove()
state = list
이런식으로 하면 안된다. state가 변화를 감지할 때 rebuild를 하게 되는데 list = super.state를 하게되면 list가 state를 받게 된다.
그래서 state = list가 결국은 state = state가 되어버려 rebuild가 일어나지 않는다... 다들 알고 있을 수도 있지만 본인은 값을 받는다고 생각해 꽤나 헤맸다 실제로 list type도 List<XFil'>로 나오니 헷갈릴 수밖에!!!
이렇게 깔끔한 UI가 완성되었다!
<복붙용 코드>
class ImagePickerService {
static final ImagePickerService _imagePickerService =
ImagePickerService._internal();
factory ImagePickerService() {
return _imagePickerService;
}
ImagePickerService._internal();
final ImagePicker _picker = ImagePicker();
Future<List<XFile>> pickImage() async {
try {
final pickedFile = await _picker.pickMultiImage();
return pickedFile;
} catch (e) {
print('ImagePickerService: $e');
return [];
}
}
Future<XFile?> pickSingleImage() async {
try {
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
return pickedFile;
} catch (e) {
print('ImagePickerService: $e');
return null;
}
}
}
class ImageState extends StateNotifier<List<XFile>> {
ImageState() : super(<XFile>[]);
final ImagePickerService picker = ImagePickerService();
set state(List<XFile> value) {
super.state = value;
}
delImage(XFile image) {
var list = [...super.state];
list.remove(image);
state = list;
}
void addImage(List<XFile> value) {
var list = [...super.state];
if (list.isEmpty) {
state = value;
} else {
list.addAll(value);
list.toSet().toList();
state = list;
}
if (super.state.length > 5) {
state = super.state.sublist(0, 5);
Fluttertoast.showToast(msg: 'You can only upload 5 images');
}
}
Future getImage() async {
picker.pickImage().then((value) {
addImage(value);
}).catchError((onError) {
Fluttertoast.showToast(msg: 'failed to get image');
});
}
}
final imagePickerProvider =
StateNotifierProvider<ImageState, List<XFile>>((ref) {
return ImageState();
});
class ImageWidget extends ConsumerWidget {
const ImageWidget({Key? key}) : super(key: key);
Widget build(BuildContext context, WidgetRef ref) {
double imgBoxSize = ((MediaQuery.of(context).size.width - 32) / 5) - 4;
final images = ref.watch(imagePickerProvider);
Widget imageBox(XFile img) => GestureDetector(
onTap: () => ref.read(imagePickerProvider.notifier).delImage(img),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
width: imgBoxSize,
height: imgBoxSize,
child: Stack(children: [
Center(
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: Image.file(File(img.path)).image),
borderRadius: BorderRadius.circular(10)),
width: imgBoxSize,
height: imgBoxSize)),
Positioned(
top: 0,
right: 0,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(10)),
child: Icon(Icons.close,
size: 15, color: Colors.grey[400])))
])));
return Row(children: [
if (images.length == 5) ...[
...images.map((e) => imageBox(e)).toList(),
] else ...[
...images.map((e) => imageBox(e)).toList(),
InkWell(
onTap: () => ref.read(imagePickerProvider.notifier).getImage(),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
width: MediaQuery.of(context).size.width * 0.17,
height: MediaQuery.of(context).size.width * 0.17,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!, width: 1),
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.image, color: Colors.grey[400]!),
const SizedBox(
height: 5,
),
Text(
'image',
style: TextStyle(
color: Colors.grey[400],
fontWeight: FontWeight.w500,
fontSize: 12.0),
)
],
),
))
]
]);
}
}